/*license*\
XBN-Java: Copyright (C) 2014, Jeff Epstein (aliteralmind __DASH__ github __AT__ yahoo __DOT__ com)
This software is dual-licensed under the:
- Lesser General Public License (LGPL) version 3.0 or, at your option, any later version;
- Apache Software License (ASL) version 2.0.
Either license may be applied at your discretion. More information may be found at
- http://en.wikipedia.org/wiki/Multi-licensing.
The text of both licenses is available in the root directory of this project, under the names "LICENSE_lgpl-3.0.txt" and "LICENSE_asl-2.0.txt". The latest copies may be downloaded at:
- LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
\*license*/
package com.github.xbn.testdev;
import com.github.xbn.io.NewPrintWriterToFile;
import com.github.xbn.io.NewTextAppenterFor;
import com.github.xbn.io.PlainTextFileUtil;
import com.github.xbn.io.RTIOException;
import com.github.xbn.io.TextAppenter;
import com.github.xbn.lang.CrashIfObject;
import com.github.xbn.number.NumberUtil;
import com.github.xbn.text.StringUtil;
import com.github.xbn.util.tuple.FourTuple;
import com.github.xbn.util.tuple.ThreeTuple;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.Iterator;
import java.util.Objects;
import static com.github.xbn.lang.XbnConstants.*;
/**
<p>Change tab-indentation in a file or directory of files, to spaces.</p>
{@.codelet com.github.xbn.examples.testdev.ReplaceAllIndentTabsWithSpacesXmpl%lineRange(1, false, "allFileTypesKeepFilter =", 1, false, "new ReplaceAllIndentTabsWithSpaces", "^ ")}
* @since 0.1.0
* @author Copyright (C) 2014, Jeff Epstein ({@code aliteralmind __DASH__ github __AT__ yahoo __DOT__ com}), dual-licensed under the LGPL (version 3.0 or later) or the ASL (version 2.0). See source code for details. <a href="http://xbnjava.aliteralmind.com">{@code http://xbnjava.aliteralmind.com}</a>, <a href="https://github.com/aliteralmind/xbnjava">{@code https://github.com/aliteralmind/xbnjava}</a>
**/
public class ReplaceAllIndentTabsWithSpaces {
private final TextAppenter debug;
private final String spaces;
private final TabToSpaceDebugLevel dbgLevel;
private static final DecimalFormat DEC_FMT = new DecimalFormat("#.###");
/**
<p>Create a new instance from space-count and optional debugging.</p>
* <p>Equal to
<br/> <code>{@link #ReplaceAllIndentTabsWithSpaces(int, Appendable, TabToSpaceDebugLevel) this}(space_count, debug_ifNonNull, {@link TabToSpaceDebugLevel}.{@link TabToSpaceDebugLevel#ALL_SUMMARY_ONLY ALL_SUMMARY_ONLY})</code></p>
*/
public ReplaceAllIndentTabsWithSpaces(int space_count, Appendable debug_ifNonNull) {
this(space_count, debug_ifNonNull, TabToSpaceDebugLevel.ALL_SUMMARY_ONLY);
}
/**
<p>Create a new instance from space-count and optional debugging.</p>
* @param space_count The number of spaces to replace each tab with. If zero, indentation is eliminated. May not be less than zero. Get with {@link #getSpaces() getSpaces}{@code ()}
* @param debug_ifNonNull Get with {@link #getDebugAptr() getDebugAptr}{@code ()}.
* @param debug_level If {@code debug_ifNonNull} is non-{@code null}, this may not be {@code null}. Get with {@link #getDebugLevel() getDebugLevel}{@code ()}. If<ul>
<li>{@link TabToSpaceDebugLevel#OFF OFF}: Nothing is debugged.</li>
<li>{@link TabToSpaceDebugLevel#ALL_SUMMARY_ONLY ALL_SUMMARY_ONLY}: A single summary after all files are processed.</li>
<li>{@link TabToSpaceDebugLevel#FILE_SUMMARIES FILE_SUMMARIES}: In addition to the all-summary, a single dot is printed for every file.</li>
<li>{@link TabToSpaceDebugLevel#FILE_DOTS FILE_DOTS}: In addition to the all-summary, a summary is printed for every file.</li>
<li>{@link TabToSpaceDebugLevel#LINE_COUNTS LINE_COUNTS}: In addition to the above, this also outputs a single character for every line, representing the number of tabs replaced in it (when one or greater). This uses <code>{@link com.github.xbn.number.NumberUtil}.{@link com.github.xbn.number.NumberUtil#CHAR_LIST_FOR_NUMBERS_0_THROUGH_62 CHAR_LIST_FOR_NUMBERS_0_THROUGH_62}</code>.</li>
</ul>
* @exception NegativeArraySizeException If {@code space_count} is less than zero.
* @see #ReplaceAllIndentTabsWithSpaces(int, Appendable)
*/
public ReplaceAllIndentTabsWithSpaces(int space_count, Appendable debug_ifNonNull, TabToSpaceDebugLevel debug_level) {
spaces = StringUtil.getStringOfLengthAllCharsEqualTo(space_count, ' ', "space_count");
debug = NewTextAppenterFor.appendableUnusableIfNull(debug_ifNonNull);
if(debug_ifNonNull != null) {
Objects.requireNonNull(debug_level, "debug_level");
dbgLevel = debug_level;
} else {
dbgLevel = null;
}
}
/**
<p>The string-of-spaces, which replaces a single tab.</p>
* @see #ReplaceAllIndentTabsWithSpaces(int, Appendable, TabToSpaceDebugLevel)
*/
public String getSpaces() {
return spaces;
}
/**
<p>The debugging appenter.</p>
* @see #getDebugLevel()
* @see #ReplaceAllIndentTabsWithSpaces(int, Appendable, TabToSpaceDebugLevel)
*/
public TextAppenter getDebugAptr() {
return debug;
}
public TabToSpaceDebugLevel getDebugLevel() {
return dbgLevel;
}
/**
<p>Replace all indentation tabs for all files in a directory, with runtime errors only--<i>this overwrites the files</i>.</p>
* <p>Equal to
<br/> <code>{@link #overwriteDirectoryX(Iterator) overwriteDirectoryX}(file_itr)</code></p>
* @exception RTIOException If a {@code java.io.IOException} is thrown for any reason.
*/
public FourTuple<Long,Long,Long,Integer> overwriteDirectory(Iterator<File> file_itr) {
try {
return overwriteDirectoryX(file_itr);
} catch(IOException iox) {
throw new RTIOException(iox);
}
}
/**
<p>Replace all indentation tabs for all files in a directory--<i>this overwrites the files</i>.</p>
<p>For each file, this<ol>
<li>{@linkplain com.github.xbn.io.PlainTextFileUtil#getText(File, String) Reads in} the text,</li>
<li>Creates a {@linkplain com.github.xbn.text.StringUtil#getLineIterator(Object) line-iterator} to it,</li>
<li>Opens a {@linkplain com.github.xbn.io.NewPrintWriterToFile print-writer} to the <i>original file</i> (with overwrite and auto-flush), and</li>
<li>calls
<br/> <code>{@link #appendForFileX(Iterator, Appendable) appendForFileX}(<i>[line-iterator]</i>, <i>[print-writer]</i>)</code></li>
</ol></p>
* @param file_itr May not be {@code null}.
* @return A four-tuple containing, in order, the total number of lines in all files, the total lines containing at least one tab, the total tabs replaced, and the number of files processed.
* @exception RTIOException If an {@link java.io.IOException} is thrown for any reason.
* @see #overwriteDirectory(Iterator)
* @see org.apache.commons.io.FileUtils#iterateFiles(File, IOFileFilter, IOFileFilter) commons.io.FileUtils#iterateFiles
*/
public FourTuple<Long,Long,Long,Integer> overwriteDirectoryX(Iterator<File> file_itr) throws IOException {
int totalFiles = 0;
long totalLines = 0;
long tabLines = 0;
long tabsReplaced = 0;
File f = null;
try {
while(file_itr.hasNext()) {
f = file_itr.next();
if(getDebugAptr().isUseable()) {
if(getDebugLevel().isOnAndAtLeast(
TabToSpaceDebugLevel.FILE_DOTS)) {
getDebugAptr().appent(".");
} else if(getDebugLevel().isOnAndAtLeast(
TabToSpaceDebugLevel.FILE_SUMMARIES)) {
getDebugAptr().appent(f.getAbsolutePath() + ": ");
}
}
String s = PlainTextFileUtil.getText(f, f.getAbsolutePath());
Iterator<String> lineItr = StringUtil.getLineIterator(s);
PrintWriter fileTextWriter = new NewPrintWriterToFile().overwrite().
manualFlush().build(f.getAbsolutePath());
ThreeTuple<Long,Long,Long> stats = appendForFileX(lineItr, fileTextWriter);
fileTextWriter.flush();
fileTextWriter.close();
totalFiles++;
totalLines += stats.get1();
tabLines += stats.get2();
tabsReplaced += stats.get2();
}
} catch(IOException iox) {
if(f == null) {
throw iox;
}
throw new RTIOException("File: " + f.getName(), iox);
} catch(RuntimeException rx) {
throw CrashIfObject.nullOrReturnCause(file_itr, "file_itr", null, rx);
}
if(getDebugAptr().isUseable() && !getDebugLevel().isOff()) {
getDebugAptr().appentln("ALL FILES: " + getSummaryFromStats(totalFiles, totalLines, tabLines, tabsReplaced));
}
return new FourTuple<Long,Long,Long,Integer>(totalLines, tabLines, tabsReplaced, totalFiles);
}
/**
<p>Replace all indentation tabs in a single file, with runtime errors only.</p>
* <p>Equal to
<br/> <code>{@link #appendForFileX(Iterator, Appendable) appendForFileX}(line_itr, output)</code></p>
* @exception RTIOException If an {@link java.io.IOException} is thrown for any reason.
*/
public ThreeTuple<Long,Long,Long> appendForFile(Iterator<String> line_itr, Appendable output) {
try {
return appendForFileX(line_itr, output);
} catch(IOException iox) {
throw new RTIOException(iox);
}
}
/**
<p>Replace all indentation tabs in a single file.</p>
* @param line_itr May not be {@code null}.
* @param to_appendTo Where output should be written. May not be {@code null}.
* @return A three-tuple containing, in order, the total number of lines in the file, the total lines containing at least one tab, and the total tabs replaced.
* @see #appendForFile(Iterator, Appendable) appendForFile
* @see #overwriteDirectoryX(Iterator)
*/
public ThreeTuple<Long,Long,Long> appendForFileX(Iterator<String> line_itr, Appendable to_appendTo) throws IOException {
long totalLines = 0;
long tabLines = 0;
long tabsReplaced = 0;
try {
while(line_itr.hasNext()) {
totalLines++;
String line = line_itr.next();
if(line.length() == 0) {
to_appendTo.append(line).append(LINE_SEP);
continue;
}
int charIdx = 0;
while(charIdx < line.length() && line.charAt(charIdx) == '\t') {
charIdx++;
to_appendTo.append(getSpaces());
//getDebugAptr().pauseIfUseable("[" + totalLines + "." + charIdx + "]: \"" + getSpaces() + "\"");
}
tabsReplaced += charIdx;
tabLines++;
if(charIdx > 0 && getDebugAptr().isUseable() &&
getDebugLevel().isLineCounts()) {
getDebugAptr().appent(NumberUtil.getCharForNumber0Through62(charIdx));
}
to_appendTo.append(line.substring(charIdx)).append(LINE_SEP);
}
} catch(RuntimeException rx) {
CrashIfObject.nnull(line_itr, "line_itr", null);
throw CrashIfObject.nullOrReturnCause(to_appendTo, "to_appendTo", null, rx);
}
if(getDebugAptr().isUseable() &&
getDebugLevel().isOnAndAtLeast(
TabToSpaceDebugLevel.FILE_SUMMARIES)) {
getDebugAptr().appentln();
getDebugAptr().appent(" - " + getSummaryFromStats(-1, totalLines, tabLines, tabsReplaced));
}
return new ThreeTuple<Long,Long,Long>(totalLines, tabLines, tabsReplaced);
}
public static final String getSummaryFromStats(FourTuple<Long,Long,Long,Integer> stats) {
try {
return getSummaryFromStats(stats.get4(), stats.get1(), stats.get2(), stats.get3());
} catch(RuntimeException rx) {
throw CrashIfObject.nullOrReturnCause(stats, "stats", null, rx);
}
}
public static final String getSummaryFromStats(ThreeTuple<Long,Long,Long> stats) {
try {
return getSummaryFromStats(-1, stats.get1(), stats.get2(), stats.get3());
} catch(RuntimeException rx) {
throw CrashIfObject.nullOrReturnCause(stats, "stats", null, rx);
}
}
public static final String getSummaryFromStats(int totalFiles_neg1IfIgnore, long total_lines, long tab_lines, long tabs_replaced) {
StringBuilder bldr = new StringBuilder();
if(totalFiles_neg1IfIgnore != -1) {
bldr.append("total files=").append(totalFiles_neg1IfIgnore).append(", ");
}
bldr.append("total lines=").append(total_lines + ", ");
if(totalFiles_neg1IfIgnore != -1) {
bldr.append("average lines-per-file=").append(DEC_FMT.format(
total_lines / new Double(totalFiles_neg1IfIgnore))).append(", ");
}
bldr.append("with tabs=").append(tab_lines).
append(", tabs replaced=").append(tabs_replaced).
append(", average tabs-per-line=").append(DEC_FMT.format(
tabs_replaced / new Double(total_lines)));
return bldr.toString();
}
}