/* * Copyright 2016 DiffPlug * * 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.diffplug.spotless; import static com.diffplug.spotless.LibPreconditions.requireElementsNonNull; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import javax.annotation.Nullable; /** Formatter which performs the full formatting. */ public final class Formatter implements Serializable { private static final long serialVersionUID = 1L; private LineEnding.Policy lineEndingsPolicy; private Charset encoding; private Path rootDir; private List<FormatterStep> steps; private FormatExceptionPolicy exceptionPolicy; private Formatter(LineEnding.Policy lineEndingsPolicy, Charset encoding, Path rootDirectory, List<FormatterStep> steps, FormatExceptionPolicy exceptionPolicy) { this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy, "lineEndingsPolicy"); this.encoding = Objects.requireNonNull(encoding, "encoding"); this.rootDir = Objects.requireNonNull(rootDirectory, "rootDir"); this.steps = requireElementsNonNull(new ArrayList<>(steps)); this.exceptionPolicy = Objects.requireNonNull(exceptionPolicy, "exceptionPolicy"); } // override serialize output private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(lineEndingsPolicy); out.writeObject(encoding.name()); out.writeObject(rootDir.toString()); out.writeObject(steps); out.writeObject(exceptionPolicy); } // override serialize input @SuppressWarnings("unchecked") private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { lineEndingsPolicy = (LineEnding.Policy) in.readObject(); encoding = Charset.forName((String) in.readObject()); rootDir = Paths.get((String) in.readObject()); steps = (List<FormatterStep>) in.readObject(); exceptionPolicy = (FormatExceptionPolicy) in.readObject(); } // override serialize input @SuppressWarnings("unused") private void readObjectNoData() throws ObjectStreamException { throw new UnsupportedOperationException(); } public LineEnding.Policy getLineEndingsPolicy() { return lineEndingsPolicy; } public Charset getEncoding() { return encoding; } public Path getRootDir() { return rootDir; } public List<FormatterStep> getSteps() { return steps; } public FormatExceptionPolicy getExceptionPolicy() { return exceptionPolicy; } public static Formatter.Builder builder() { return new Formatter.Builder(); } public static class Builder { // required parameters private LineEnding.Policy lineEndingsPolicy; private Charset encoding; private Path rootDir; private List<FormatterStep> steps; private FormatExceptionPolicy exceptionPolicy; private Builder() {} public Builder lineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) { this.lineEndingsPolicy = lineEndingsPolicy; return this; } public Builder encoding(Charset encoding) { this.encoding = encoding; return this; } public Builder rootDir(Path rootDir) { this.rootDir = rootDir; return this; } public Builder steps(List<FormatterStep> steps) { this.steps = steps; return this; } public Builder exceptionPolicy(FormatExceptionPolicy exceptionPolicy) { this.exceptionPolicy = exceptionPolicy; return this; } public Formatter build() { return new Formatter(lineEndingsPolicy, encoding, rootDir, steps, exceptionPolicy == null ? FormatExceptionPolicy.failOnlyOnError() : exceptionPolicy); } } /** Returns true iff the given file's formatting is up-to-date. */ public boolean isClean(File file) throws IOException { Objects.requireNonNull(file); String raw = new String(Files.readAllBytes(file.toPath()), encoding); String unix = LineEnding.toUnix(raw); // check the newlines (we can find these problems without even running the steps) int totalNewLines = (int) unix.codePoints().filter(val -> val == '\n').count(); int windowsNewLines = raw.length() - unix.length(); if (lineEndingsPolicy.isUnix(file)) { if (windowsNewLines != 0) { return false; } } else { if (windowsNewLines != totalNewLines) { return false; } } // check the other formats String formatted = compute(unix, file); // return true iff the formatted string equals the unix one return formatted.equals(unix); } /** Applies formatting to the given file. */ public void applyTo(File file) throws IOException { applyToAndReturnResultIfDirty(file); } /** * Applies formatting to the given file. * * Returns null if the file was already clean, or the * formatted result with unix newlines if it was not. */ public @Nullable String applyToAndReturnResultIfDirty(File file) throws IOException { Objects.requireNonNull(file); byte[] rawBytes = Files.readAllBytes(file.toPath()); String raw = new String(rawBytes, encoding); String rawUnix = LineEnding.toUnix(raw); // enforce the format String formattedUnix = compute(rawUnix, file); // enforce the line endings String formatted = computeLineEndings(formattedUnix, file); // write out the file iff it has changed byte[] formattedBytes = formatted.getBytes(encoding); if (!Arrays.equals(rawBytes, formattedBytes)) { Files.write(file.toPath(), formattedBytes, StandardOpenOption.TRUNCATE_EXISTING); return formattedUnix; } else { return null; } } /** Applies the appropriate line endings to the given unix content. */ public String computeLineEndings(String unix, File file) { Objects.requireNonNull(unix, "unix"); Objects.requireNonNull(file, "file"); String ending = lineEndingsPolicy.getEndingFor(file); if (!ending.equals(LineEnding.UNIX.str())) { return unix.replace(LineEnding.UNIX.str(), ending); } else { return unix; } } /** * Returns the result of calling all of the FormatterSteps. * The input must have unix line endings, and the output * is guaranteed to also have unix line endings. */ public String compute(String unix, File file) { Objects.requireNonNull(unix, "unix"); Objects.requireNonNull(file, "file"); for (FormatterStep step : steps) { try { String formatted = step.format(unix, file); if (formatted == null) { // This probably means it was a step that only checks // for errors and doesn't actually have any fixes. // No exception was thrown so we can just continue. } else { // Should already be unix-only, but some steps might misbehave. unix = LineEnding.toUnix(formatted); } } catch (Throwable e) { String relativePath = rootDir.relativize(file.toPath()).toString(); exceptionPolicy.handleError(e, step, relativePath); } } return unix; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + encoding.hashCode(); result = prime * result + lineEndingsPolicy.hashCode(); result = prime * result + rootDir.hashCode(); result = prime * result + steps.hashCode(); result = prime * result + exceptionPolicy.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Formatter other = (Formatter) obj; return encoding.equals(other.encoding) && lineEndingsPolicy.equals(other.lineEndingsPolicy) && rootDir.equals(other.rootDir) && steps.equals(other.steps) && exceptionPolicy.equals(other.exceptionPolicy); } }