| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.wave.pst.style; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.CharStreams; |
| import com.google.common.io.Files; |
| |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.ArrayDeque; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * A code styler using a Builder approach to configure styles using smaller |
| * reformatting components. |
| * |
| * TODO(kalman): take string literals into account. |
| * |
| * @author kalman@google.com (Benjamin Kalman) |
| */ |
| public final class PstStyler implements Styler { |
| |
| private static final String BACKUP_SUFFIX = ".prePstStyler"; |
| private static final String INDENT = " "; |
| private static final String[] ATOMIC_TOKENS = new String[] { |
| "} else {", |
| "} else if (", |
| "for (", |
| "/*-{", |
| "}-*/", |
| }; |
| |
| /** |
| * Builder for a series of composed style components. |
| */ |
| private static class StyleBuilder { |
| |
| /** |
| * Styles a single line, outputting generated lines to a generator. |
| * The styler may output anywhere between 0 and infinite lines. |
| */ |
| private interface LineStyler { |
| void next(String line, LineGenerator generator); |
| } |
| |
| /** |
| * Generates lines to some output sink. |
| */ |
| private interface LineGenerator { |
| void yield(CharSequence s); |
| } |
| |
| /** |
| * A pipeline of line stylers as a single line generator. |
| */ |
| private static final class LinePipeline implements LineGenerator { |
| private final LineStyler lineStyler; |
| private final LineGenerator next; |
| |
| private LinePipeline(LineStyler lineStyler, LineGenerator next) { |
| this.lineStyler = lineStyler; |
| this.next = next; |
| } |
| |
| /** |
| * Constructs a pipeline of line stylers. |
| * |
| * @param ls the line stylers to place in the pipeline |
| * @param sink the line generator at the end of the pipeline |
| * @return the head of the pipeline |
| */ |
| public static LinePipeline construct(Iterable<LineStyler> ls, LineGenerator sink) { |
| return new LinePipeline(head(ls), |
| (Iterables.size(ls) == 1) ? sink : construct(tail(ls), sink)); |
| } |
| |
| private static LineStyler head(Iterable<LineStyler> ls) { |
| return ls.iterator().next(); |
| } |
| |
| private static Iterable<LineStyler> tail(final Iterable<LineStyler> ls) { |
| return new Iterable<LineStyler>() { |
| @Override public Iterator<LineStyler> iterator() { |
| Iterator<LineStyler> tail = ls.iterator(); |
| tail.next(); |
| return tail; |
| } |
| }; |
| } |
| |
| @Override |
| public void yield(CharSequence s) { |
| lineStyler.next(s.toString(), next); |
| } |
| } |
| |
| /** |
| * Generates lines into a list. |
| */ |
| private static final class ListGenerator implements LineGenerator { |
| private final List<String> list = Lists.newArrayList(); |
| |
| @Override |
| public void yield(CharSequence s) { |
| list.add(s.toString()); |
| } |
| |
| public List<String> getList() { |
| return list; |
| } |
| } |
| |
| /** |
| * Maintains some helpful state across a styling. |
| */ |
| private abstract class StatefulLineStyler implements LineStyler { |
| private boolean inShortComment = false; |
| private boolean inLongComment = false; |
| private boolean inStartOfComment = false; |
| private boolean previousWasComment = false; |
| private int lineNumber = 0; |
| |
| protected boolean inComment() { |
| return inShortComment || inLongComment; |
| } |
| |
| protected boolean inStartOfComment() { |
| return inStartOfComment; |
| } |
| |
| protected boolean inLongComment() { |
| return inLongComment; |
| } |
| |
| protected int getLineNumber() { |
| return lineNumber; |
| } |
| |
| protected boolean previousWasComment() { |
| return previousWasComment; |
| } |
| |
| @Override |
| public final void next(String line, LineGenerator generator) { |
| lineNumber++; |
| // TODO(kalman): JSNI? |
| if (line.matches("^[ \t]*/\\*.*")) { |
| inLongComment = true; |
| inStartOfComment = true; |
| } |
| if (line.matches("^[ \t]*//.*")) { |
| inShortComment = true; |
| inStartOfComment = true; |
| } |
| doNext(line, generator); |
| previousWasComment = inShortComment || inLongComment; |
| if (line.contains("*/")) { |
| inLongComment = false; |
| } |
| inShortComment = false; |
| inStartOfComment = false; |
| } |
| |
| abstract void doNext(String line, LineGenerator generator); |
| } |
| |
| private final List<LineStyler> lineStylers = Lists.newArrayList(); |
| |
| /** |
| * Applies the state of the styler to a list of lines. |
| * |
| * @return the styled lines |
| */ |
| public List<String> apply(List<String> lines) { |
| ListGenerator result = new ListGenerator(); |
| LinePipeline pipeline = LinePipeline.construct(lineStylers, result); |
| for (String line : lines) { |
| pipeline.yield(line); |
| } |
| return result.getList(); |
| } |
| |
| public StyleBuilder addNewLineBefore(final char newLineBefore) { |
| lineStylers.add(new StatefulLineStyler() { |
| @Override public void doNext(String line, LineGenerator generator) { |
| // TODO(kalman): this is heavy-handed; be fine-grained and just don't |
| // split over tokens (need regexp, presumably). |
| if (inComment() || containsAtomicToken(line)) { |
| generator.yield(line); |
| return; |
| } |
| |
| StringBuilder s = new StringBuilder(); |
| for (char c : line.toCharArray()) { |
| if (c == newLineBefore) { |
| generator.yield(s); |
| s = new StringBuilder(); |
| } |
| s.append(c); |
| } |
| generator.yield(s); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder addNewLineAfter(final char newLineAfter) { |
| lineStylers.add(new StatefulLineStyler() { |
| @Override public void doNext(String line, LineGenerator generator) { |
| // TODO(kalman): same as above. |
| if (inComment() || containsAtomicToken(line)) { |
| generator.yield(line); |
| return; |
| } |
| |
| StringBuilder s = new StringBuilder(); |
| for (char c : line.toCharArray()) { |
| s.append(c); |
| if (c == newLineAfter) { |
| generator.yield(s); |
| s = new StringBuilder(); |
| } |
| } |
| generator.yield(s); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder trim() { |
| lineStylers.add(new LineStyler() { |
| @Override public void next(String line, LineGenerator generator) { |
| generator.yield(line.trim()); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder removeRepeatedSpacing() { |
| lineStylers.add(new LineStyler() { |
| @Override public void next(String line, LineGenerator generator) { |
| generator.yield(line.replaceAll("[ \t]+", " ")); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder stripBlankLines() { |
| lineStylers.add(new LineStyler() { |
| @Override public void next(String line, LineGenerator generator) { |
| if (!line.isEmpty()) { |
| generator.yield(line); |
| } |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder stripInitialBlankLine() { |
| lineStylers.add(new LineStyler() { |
| boolean firstLine = true; |
| @Override public void next(String line, LineGenerator generator) { |
| if (!firstLine || !line.isEmpty()) { |
| generator.yield(line); |
| } |
| firstLine = false; |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder stripDuplicateBlankLines() { |
| lineStylers.add(new LineStyler() { |
| boolean previousWasEmpty = false; |
| @Override public void next(String line, LineGenerator generator) { |
| if (!previousWasEmpty || !line.isEmpty()) { |
| generator.yield(line); |
| } |
| previousWasEmpty = line.isEmpty(); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder indentBraces() { |
| lineStylers.add(new StatefulLineStyler() { |
| private int indentLevel = 0; |
| |
| @Override public void doNext(String line, LineGenerator generator) { |
| if (!ignore(line) && line.contains("}")) { |
| indentLevel--; |
| Preconditions.checkState(indentLevel >= 0, |
| "Indentation level reached < 0 on line " + getLineNumber() + " (" + line + ")"); |
| } |
| String result = ""; |
| if (!line.isEmpty()) { |
| result = Strings.repeat(INDENT, indentLevel) + line; |
| } |
| if (!ignore(line) && line.contains("{")) { |
| indentLevel++; |
| } |
| generator.yield(result.toString()); |
| } |
| |
| private boolean ignore(String line) { |
| // Ignore self-closing braces. |
| return line.contains("{") |
| && line.contains("}") |
| && line.indexOf('{') < line.lastIndexOf('}'); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder indentLongComments() { |
| lineStylers.add(new StatefulLineStyler() { |
| @Override void doNext(String line, LineGenerator generator) { |
| if (inLongComment() && !inStartOfComment()) { |
| generator.yield(" " + line); |
| } else { |
| generator.yield(line); |
| } |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder doubleIndentUnfinishedLines() { |
| lineStylers.add(new StatefulLineStyler() { |
| boolean previousUnfinished = false; |
| |
| @Override public void doNext(String line, LineGenerator generator) { |
| generator.yield((previousUnfinished ? Strings.repeat(INDENT, 2) : "") + line); |
| previousUnfinished = |
| !inComment() && |
| !line.matches("^.*[;{},\\-/]$") && // Ends with an expected character. |
| !line.contains("@Override") && // or an annotation. |
| !line.isEmpty() && |
| !line.contains("//"); // Single-line comment. |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder addBlankLineBeforeMatching(final String regex) { |
| lineStylers.add(new StatefulLineStyler() { |
| @Override public void doNext(String line, LineGenerator generator) { |
| if ((!inComment() || inStartOfComment()) && line.matches(regex)) { |
| generator.yield(""); |
| } |
| generator.yield(line); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder addBlankLineBeforeClasslikeWithNoPrecedingComment() { |
| lineStylers.add(new StatefulLineStyler() { |
| @Override public void doNext(String line, LineGenerator generator) { |
| if (!previousWasComment() |
| && line.matches(".*\\b(class|interface|enum)\\b.*")) { |
| generator.yield(""); |
| } |
| generator.yield(line); |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder addBlankLineAfterBraceUnlessInMethod() { |
| lineStylers.add(new StatefulLineStyler() { |
| // true for every level of braces that is a class-like construct |
| ArrayDeque<Boolean> stack = new ArrayDeque<Boolean>(); |
| boolean sawClasslike = false; |
| |
| @Override public void doNext(String line, LineGenerator generator) { |
| if (inComment()) { |
| generator.yield(line); |
| } else if (line.endsWith("}") && !line.contains("{")) { |
| generator.yield(line); |
| stack.pop(); |
| if (!stack.isEmpty() && stack.peek()) { |
| generator.yield(""); |
| } |
| } else { |
| // Perhaps we could match anonymous classes by adding "new" here, |
| // but this is not currently needed. |
| if (line.matches(".*\\b(class|interface|enum)\\b.*")) { |
| sawClasslike = true; |
| } |
| if (line.endsWith("{")) { |
| if (line.contains("}")) { |
| stack.pop(); |
| } |
| stack.push(sawClasslike); |
| sawClasslike = false; |
| } else if (line.endsWith(";")) { |
| sawClasslike = false; |
| } |
| generator.yield(line); |
| } |
| } |
| }); |
| return this; |
| } |
| |
| public StyleBuilder addBlankLineAfterMatching(final String regex) { |
| lineStylers.add(new StatefulLineStyler() { |
| boolean previousLineMatched = false; |
| |
| @Override public void doNext(String line, LineGenerator generator) { |
| if (previousLineMatched) { |
| generator.yield(""); |
| } |
| generator.yield(line); |
| previousLineMatched = line.matches(regex); |
| } |
| }); |
| return this; |
| } |
| |
| private boolean containsAtomicToken(String line) { |
| for (String token : ATOMIC_TOKENS) { |
| if (line.contains(token)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| |
| @Override |
| public void style(File f, boolean saveBackup) { |
| List<String> lines = null; |
| try { |
| lines = CharStreams.readLines(new FileReader(f)); |
| } catch (IOException e) { |
| System.err.println("Couldn't find file " + f.getName() + " to style: " + e.getMessage()); |
| return; |
| } |
| |
| Joiner newlineJoiner = Joiner.on('\n'); |
| |
| if (saveBackup) { |
| File backup = new File(f.getAbsolutePath() + BACKUP_SUFFIX); |
| try { |
| Files.write(newlineJoiner.join(lines), backup, Charset.defaultCharset()); |
| } catch (IOException e) { |
| System.err.println("Couldn't write backup " + backup.getName() + ": " + e.getMessage()); |
| return; |
| } |
| } |
| |
| try { |
| Files.write(newlineJoiner.join(styleLines(lines)), f, Charset.defaultCharset()); |
| } catch (IOException e) { |
| System.err.println("Couldn't write styled file " + f.getName() + ": " + e.getMessage()); |
| return; |
| } |
| } |
| |
| private List<String> styleLines(List<String> lines) { |
| return new StyleBuilder() |
| .trim() |
| .removeRepeatedSpacing() |
| .addNewLineBefore('}') |
| .addNewLineAfter('{') |
| .addNewLineAfter('}') |
| .addNewLineAfter(';') |
| .trim() |
| .removeRepeatedSpacing() |
| .stripBlankLines() |
| .trim() |
| .indentBraces() |
| .indentLongComments() |
| .addBlankLineBeforeMatching("[ \t]*@Override.*") |
| .addBlankLineBeforeMatching(".*/\\*\\*.*") |
| .addBlankLineAfterMatching("package.*") |
| .addBlankLineBeforeMatching("package.*") |
| .addBlankLineBeforeClasslikeWithNoPrecedingComment() |
| .addBlankLineAfterBraceUnlessInMethod() |
| .stripDuplicateBlankLines() |
| .doubleIndentUnfinishedLines() |
| .stripInitialBlankLine() |
| // TODO: blank line before first method or constructor if that has no javadoc |
| .apply(lines); |
| } |
| } |