/* * [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 } } }