/*
* [FourTidy.java]
*
* Summary: Tidy the 4DOS/4NT/TCC/TakeCommand DESCRIPT.ION and create _O_V_E_R_V_I_E_W.txt.
*
* Copyright: (c) 2001-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
*
* Licence: This software may be copied and used freely for any purpose but military.
* http://mindprod.com/contact/nonmil.html
*
* Requires: JDK 1.8+
*
* Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
*
* Version History:
* 2.0 2001-01-19 initial version
* 2.1 2004-05-30 works if manifest.txt does not exist
* 2.2 2006-03-05 reformat with IntelliJ, add Javadoc.
* 2.3 2006-03-13 move to JDK 1.5 and for:each
* 2.4 2007-06-01 handle empty DESCRIPT.ION file. Handle blank lines in DESCRIPT.ION file.
* 2.5 2007-12-31 put text results in manifest.txt instead of readme.txt. Automatic describe of manifest.txt and
* DESCRIPT.ION
* 2.6 2008-01-01 fix ConcurrentModificationBug
* 2.7 2009-05-11 change name of manifest.txt to _O_V_E_R_V_I_E_W.txt
* 2.8 2009-11-18 filter out files not from this dir from the DESCRIPT.ION file.
* 2.9 2011-02-01 allow -d -f -l option letter on the command line.
* 3.0 2011-11-07 avoid writing files back if nothing has substantially changed.
* 3.1 2012-01-10 fix bug in inOrder that caused dups to accumulate.
*/
package com.mindprod.fourtidy;
import com.mindprod.common18.EIO;
import com.mindprod.common18.InOrder;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import static java.lang.System.*;
/**
* Tidy the 4DOS/4NT/TCC/TakeCommand DESCRIPT.ION and create _O_V_E_R_V_I_E_W.txt.
*
* Works with long filenames.
*
* @author Roedy Green, Canadian Mind Products
* @version 3.1 2012-01-10 fix bug in inOrder that caused dups to accumulate.
* @since 2001-01-19
*/
final public class FourTidy
{
private static final int FIRST_COPYRIGHT_YEAR = 2001;
/**
* TCC file of descriptions
*/
private static final String DESCRIPTION_FILENAME = "DESCRIPT.ION";
/**
* undisplayed copyright notice
*/
private static final String EMBEDDED_COPYRIGHT =
"Copyright: (c) 2001-2017 Roedy Green, Canadian Mind Products, http://mindprod.com";
/**
* human readable list of descriptions we generate. This will show up in list and sort to the top
*/
private static final String OVERVIEW_FILENAME = "_O_V_E_R_V_I_E_W.txt";
/**
* Some spaces to align data in columns.
*/
private static final String padding = " ";
/**
* date this version was released
*/
private static final String RELEASE_DATE = "2012-01-10";
/**
* how to use the command line
*/
private static final String USAGE = "\nFourTidy -f (take first duplicate)\n" +
"-l (take last duplicate)\n" +
"-d (take longest most detailed duplicate, the default)";
/**
* embedded version string.
*/
private static final String VERSION_STRING = "3.1";
/**
* List of filename/description pairs.
*/
private static final ArrayList pairs = new ArrayList<>( 1000 );
/**
* longest filename. Used to align columns.
*/
private static int longestFilename = 0;
/**
* if we were to write a new descript.ion file, would anything change?
* if not, we don't need to do the work.
*/
private static boolean needToWriteDescription = false;
/**
* if we were to write a new overview.txt file, would anything change?
* if not, we don't need to do the work.
*/
private static boolean needToWriteOverview = false;
/**
* add two standard descriptions for the files we generate.
*/
private static void addStandardPairs()
{
final Pair overview = new Pair( OVERVIEW_FILENAME, "index of files with descriptions of what they are for." );
if ( !pairs.contains( overview ) )
{
pairs.add( 0, overview );
needToWriteDescription = true;
}
final Pair ion = new Pair( DESCRIPTION_FILENAME, "TCC file descriptions for TCC describe command" );
if ( !pairs.contains( ion ) )
{
pairs.add( 0, ion );
needToWriteDescription = true;
}
}
/**
* Read filename/description pairs from the DESCRIPT.ION file. Ignore blank entries.
*
* @throws IOException if can't read DESCRIPT.ION file
*/
private static void readDescriptions() throws IOException
{
// lines have form "filename" description. filename in quotes, space, description.
// O P E N
final File d = new File( DESCRIPTION_FILENAME );
if ( !d.canRead() )
{
// we just return with an empty pairs set.
needToWriteDescription = true;
return;
}
final File o = new File( OVERVIEW_FILENAME );
if ( o.exists() )
{
if ( d.exists() && Math.abs( o.lastModified() - d.lastModified() ) > 3000 )
{
// somebody edited the overview file. It was changed at a different time from the description file.
needToWriteOverview = true;
}
}
else
{
// no overview file, must recreate.
needToWriteOverview = true;
}
final BufferedReader br = new BufferedReader( new FileReader( d ), 4 * 1024/* buffsize */ );
String line;
while ( true )
{
// R E A D
line = br.readLine();
// even if line ending char not cr lf, we won't write new descript.ion just for that.
// line == null means EOF
if ( line == null )
{
break;
}
if ( line.length() == 0 )
{
/* ignore empty lines */
needToWriteDescription = true;
continue;
}
// break the line in two pieces
char terminator;
if ( line.charAt( 0 ) == '\"' )
{
/* long filename, enclosed in "s */
terminator = '\"';
line = line.substring( 1 );
}
else
{
/* short filename, terminated by space */
terminator = ' ';
}
final int fileNameEnd = line.indexOf( terminator );
// ignore null filenames
if ( fileNameEnd < 1 )
{
continue;
}
final String theFile = line.substring( 0, fileNameEnd ).trim();
// ignore null filenames and filenames outside this dir
if ( theFile.length() == 0 || theFile.indexOf( '\\' ) >= 0 || theFile.indexOf( ':' ) >= 0 )
{
needToWriteDescription = true;
continue;
}
final int descriptionEnd = line.length();
final int descriptionStart = fileNameEnd + 1;
if ( descriptionEnd - descriptionStart < 1 )
{
// ignore entry without description
needToWriteDescription = true;
continue;
}
final String thedescription =
line.substring( descriptionStart, descriptionEnd ).trim();
if ( thedescription.length() == 0 )
{
// ignore entry without description
needToWriteDescription = true;
continue;
}
// track longest filename so far
final int length = theFile.length();
if ( length > longestFilename )
{
longestFilename = length;
}
// have a good pair, add it to the list. Might be a dup. Handle that later.
pairs.add( new Pair( theFile, thedescription ) );
} // end while
// C L O S E
br.close();
}
/**
* Remove dups, keeping longest description.
*/
private static void removeDups()
{
// longest/best dup description has already been sorted first.
String prevFilename = "";
// can't use for:each since we need handle for iter.remove()
// to avoid ConcurrentModificationException
final Iterator iter = pairs.iterator();
while ( iter.hasNext() )
{
Pair aPair = ( Pair ) iter.next();
if ( aPair.filename.equalsIgnoreCase( prevFilename ) )
{
// This is a dup. Toss it out of the ArrayList
iter.remove();
needToWriteDescription = true;
}
else
{
prevFilename = aPair.filename;
}
} // end while
}
/**
* Remove descriptions if there is no corresponding file.
*/
private static void removeOrphans()
{
// we look up each file with a description to see if it exists.
// can't use for:each since we need iter.remove()
final Iterator iter = pairs.iterator();
while ( iter.hasNext() )
{
Pair aPair = ( Pair ) iter.next();
if ( !new File( aPair.filename ).exists() )
{
// no such file. Toss it out of the ArrayList
iter.remove();
needToWriteDescription = true;
}
} // end while
}
/**
* Sort entries alphabetically by filename, with longest description first for dup filenames.
*
* @param option letter d f or l to pick the correct sort for deduping.
*/
private static void sortFiles( char option )
{
final Comparator comparator;
switch ( option )
{
case 'd':
// so longest description will sort to the top to be the one taken.
comparator = new Pair.LongestFirst();
break;
case 'f':
// so the first dup originally will stay at the top to be the one taken.
comparator = new Pair.FirstDup();
break;
case 'l':
// so last dup originally will sort to the top to be the one taken.
comparator = new Pair.LastDup();
break;
default:
throw new IllegalArgumentException( "Program bug. Invalid option letter in sortFiles method." );
}
if ( !InOrder.inOrder( pairs, comparator ) )
{
Collections.sort( pairs, comparator );
needToWriteDescription = true;
}
}
/**
* Write tidied DESCRIPT.ION file.
*
* @throws IOException if can't rewrite the DESCRIPT.ION file
*/
private static void writeDescriptions() throws IOException
{
// O P E N
// file may be hidden or may not; we don't write it with hidden
// attribute.
final File f = new File( DESCRIPTION_FILENAME );
if ( f.exists() && !f.canWrite() )
{
// canWrite true implies file exists.
throw new IOException( "can't write " + DESCRIPTION_FILENAME + " file" );
}
final PrintWriter prw = EIO.getPrintWriter( f, 4 * 1024, EIO.UTF8 );
for ( Pair aPair : pairs )
{
// filename in quotes, a space, then the description.
prw.println( "\"" + aPair.filename + "\" " + aPair.description );
} // end while
// C L O S E
prw.close();
} // end writeDescriptions
/**
* Prepare human readable _O_V_E_R_V_I_E_W.txt file of file/descriptions
*
* @throws IOException if cannot write _O_V_E_R_V_I_E_W.txt
*/
private static void writeOverviewTxt() throws IOException
{
/* decide how many cols to use to display the filename */
final int colWidth = Math.min( Math.max( 12, longestFilename ), 32 ) + 1;
// O P E N
final File f = new File( OVERVIEW_FILENAME );
if ( f.exists() && !f.canWrite() )
{
// canWrite true implies file exists.
throw new IOException( "can't write " + OVERVIEW_FILENAME + " file" );
}
final PrintWriter prw = EIO.getPrintWriter( f, 4 * 1024, EIO.UTF8 );
for ( Pair aPair : pairs )
{
String pad = padding.substring( 0, Math.max( colWidth - aPair.filename
.length(), 1 ) );
prw.println( aPair.filename + pad + aPair.description );
} // end while
// C L O S E
prw.close();
}
/**
* Mainline to tidy one TCC DESCRIPT.ION file, the one in the current directory.
*
* @param args possible options -d (longest) -f (keep first dup) -l (keep last dup)
*/
public static void main( String[] args )
{
if ( !( 0 <= args.length && args.length <= 1 ) )
{
throw new IllegalArgumentException( "Must have at most one parameter\n" + USAGE );
}
final char option;
if ( args.length == 0 )
{
option = 'd';
}
else if ( args[ 0 ].equals( "-f" ) )
{
option = 'f';
}
else if ( args[ 0 ].equals( "-l" ) )
{
option = 'l';
}
else if ( args[ 0 ].equals( "-d" ) )
{
option = 'd';
}
else
{
throw new IllegalArgumentException( "Must have at most one parameter\n" + USAGE );
}
try
{
readDescriptions();
removeOrphans(); // descriptions without corresponding file.
addStandardPairs(); // put at front so -f will give them precedence.
sortFiles( option );
removeDups();
if ( needToWriteOverview || needToWriteDescription )
{
writeOverviewTxt();
}
if ( needToWriteDescription )
{
writeDescriptions();
}
}
catch ( IOException e )
{
err.println();
e.printStackTrace( err );
err.println();
}
}
}
/**
* class to represent a Filename/Description pair.
*/
final class Pair
{
/**
* used to track original order
*/
private static int counter = 0;
/**
* description for that file.
*/
final String description;
/**
* The name of one file to be described.
*/
final String filename;
private final int originalOrder;
/**
* Constructor.
*
* @param filename name of file (file.ext, no directory.
* @param description description for file
*/
Pair( String filename, String description )
{
this.originalOrder = counter++;
this.filename = filename;// don't convert to lower case. Sort compare is case-insensitive
this.description = description;
}
/**
* equal if both fields equal
*
* @param other the other Pair to compare with
*
* @return true if equal
*/
public boolean equals( Object other )
{
if ( other == null || !( other instanceof Pair ) )
{
return false;
}
Pair o = ( Pair ) other;
return this.filename.equals( o.filename ) && this.description.equals( o.description );
}
/**
* Sort first dup to the top.
*
* Defines an alternate sort order for Pair.
*/
static class FirstDup implements Comparator
{
/**
* Sort first dup to the top.
* Defines an alternate sort order for Pair with JDK 1.5+ generics.
* Compare two Pair Objects.
* Compares filename case insensitively then originalOrder numerically.
* Informally, returns (a-b), or +ve if a comes after b.
*
* @param a first Pair to compare
* @param b second Pair to compare
*
* @return +ve if a>b, 0 if a==b, -ve if a<b
*/
public final int compare( Pair a, Pair b )
{
int diff = a.filename.compareToIgnoreCase( b.filename );
if ( diff != 0 )
{
return diff;
}
return a.originalOrder - b.originalOrder;
}
}
/**
* Sort last dup to the top.
*
* Defines an alternate sort order for Pair.
*/
static class LastDup implements Comparator
{
/**
* Sort last dup to the top.
* Defines an alternate sort order for Pair with JDK 1.5+ generics.
* Compare two Pair Objects.
* Compares filename case insensitively then descending originalOrder numerically.
* Informally, returns (a-b), or +ve if a comes after b.
*
* @param a first Pair to compare
* @param b second Pair to compare
*
* @return +ve if a>b, 0 if a==b, -ve if a<b
*/
public final int compare( Pair a, Pair b )
{
int diff = a.filename.compareToIgnoreCase( b.filename );
if ( diff != 0 )
{
return diff;
}
return b.originalOrder - a.originalOrder;
}
}
/**
* Sort longest description first.
*
* Defines an alternate sort order for Pair.
*/
static class LongestFirst implements Comparator
{
/**
* Sort longest description first.
* Defines an alternate sort order for Pair with JDK 1.5+ generics.
* Compare two Pair Objects.
* Compares filename case insensitively then descending description length then descending originalOrder
* numerically.
* Informally, returns (a-b), or +ve if a comes after b.
*
* @param a first Pair to compare
* @param b second Pair to compare
*
* @return +ve if a>b, 0 if a==b, -ve if a<b
*/
public final int compare( Pair a, Pair b )
{
int diff = a.filename.compareToIgnoreCase( b.filename );
if ( diff != 0 )
{
return diff;
}
int diff1 = b.description.length() - a.description.length();
if ( diff1 != 0 )
{
return diff1;
}
return b.originalOrder - a.originalOrder; // on tie, use last dup
}
}
}