/*
* [SortCode.java]
*
* Summary: Sort multiline chunks of Java code.
*
* Copyright: (c) 2013-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:
* 1.0 2013-02-19 initial version
* 1.1 2014-04-26 add SortDeclartions, and ability to vary the comparator or supply your own.
* 1.2 2014-04-29 smarter, less buggy versions of SortDeclarations and SortMethods
* 1.3 2014-04-30 add SortPrices. Now can specify just a part of a file you want to process
* 1.4 2014-05-01 add Shuffle, -terse, -verbose, HTML manual.
* 1.5 2014-05-08 simplify code, repair parsing error
*/
package com.mindprod.sortcode;
import com.mindprod.commandline.CommandLine;
import com.mindprod.common18.EIO;
import com.mindprod.common18.ST;
import com.mindprod.fastcat.FastCat;
import com.mindprod.filter.AllButSVNDirectoriesFilter;
import com.mindprod.filter.ExtensionListFilter;
import com.mindprod.hunkio.HunkIO;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import static java.lang.System.*;
/**
* Sort multiline chunks of Java code.
*
* To use extract Java code to be sorted e.g. a set of statements, enum constants, case clauses etc.
* sortcode.jar extract.java ;
* where ; is the terminator between chunks. It might be break; or , or +++, variable length.
* The results will appear replacing the input file, sorted in a case-sensitive way with leading blanks trimmed
* and blank lines between elements. The last item may or may not have a terminator/marker.
*
* @author Roedy Green, Canadian Mind Products
* @version 1.5 2014-05-08 simplify code, repair parsing error.
* @since 2013-02-19
*/
public class SortCode
{
/**
* true if want extra output
*/
private static final boolean DEBUGGING = false;
/**
* when program first copyrighted
*/
private static final int FIRST_COPYRIGHT_YEAR = 2013;
/**
* undisplayed copyright notice
*/
@SuppressWarnings( { "UnusedDeclaration" } )
private static final String EMBEDDED_COPYRIGHT =
"Copyright: (c) 2013-2017 Roedy Green, Canadian Mind Products, http://mindprod.com";
@SuppressWarnings( { "UnusedDeclaration" } )
private static final String RELEASE_DATE = "2014-05-08";
/**
* how to use the command line
*/
private static final String USAGE = "\nsortcode.jar [-v] [-q] [-start=xxx] [-end=xxx] -marker=xxx [-comparator=xxx] [-s] dirs/files...";
/**
* embedded version string.
*/
@SuppressWarnings( { "UnusedDeclaration" } )
private static final String VERSION_STRING = "1.5";
private static int dropped = 0;
private static void addItem( final ArrayList toSort, final String item )
{
if ( item.trim().length() > 0 )
{
if ( DEBUGGING )
{
out.println( "A D D I N G " + toSort.size() );
showAbbreviated( item );
}
toSort.add( item );
}
else
{
dropped++;
}
}
/**
* analyse comparator request on command line and turn into an actual Comparator object
*
* @param comparatorName name of Comparator
*
* @return corresponding Comparator
*/
private static Comparator analyseComparator( String comparatorName )
{
switch ( comparatorName )
{
case "Sensitive":
return null;
case "Insensitive":
return String.CASE_INSENSITIVE_ORDER;
case "ReverseSensitive":
return Collections.reverseOrder( null );
case "ReverseInsensitive":
return Collections.reverseOrder( String.CASE_INSENSITIVE_ORDER );
case "Shuffle":
return new Shuffle();
case "SortClasses":
return new SortClasses();
case "SortDeclarations":
return new SortDeclarations();
case "SortMethods":
return new SortMethods();
case "SortPrices":
return new SortPrices();
case "SortBooks":
return new SortBooks();
default:
if ( !ST.contains( comparatorName, '.' ) )
{
err.println( "-comparator (note the lead -) must be fully qualified e.g. com.mindprod.sortcode.SortDeclarations" );
err.println( "that implement Comparator" );
err.println( "or be one of the built-in classes: Sensitive, Insensitive, ReverseSensitive, ReverseInsensitive, " );
err.println( "Shuffle, SortDeclarations, SortMethods, SortClasses, SortPrices, SortBooks..." );
err.println( "You requested -comparator=\"" + comparatorName + "\"" );
System.exit( 2 );
return null;
}
else
{
return dynamicallyLoadComparator( comparatorName );
}
} // end switch
} // /method
/**
* dynamically load a comparator whose name we found on the command line
*
* @param comparatorName fullly qualified name of Comparator to load e.g. com.mindprod.sortCode.Shuffle
*
* @return reference to Comparator object
*/
@SuppressWarnings( "unchecked" )
private static Comparator dynamicallyLoadComparator( final String comparatorName )
{
// dynamically load the comparator by name
try
{
// we have no way of knowing for certain if this is really
// is a Comparator
// There is no record of the generics in the Comparator class file.
return ( Comparator ) ( Class.forName( comparatorName ).newInstance() );
}
catch ( ClassNotFoundException e )
{
err.println( "ClassNotFoundException: cannot load " + comparatorName );
System.exit( 2 );
return null;
}
catch ( InstantiationException e )
{
err.println( "InstantiationException: cannot load " + comparatorName );
System.exit( 2 );
return null;
}
catch ( IllegalAccessException e )
{
err.println( "IllegalAccessException: cannot load " + comparatorName );
System.exit( 2 );
return null;
}
}
/**
* debugging tool to display long strings
*
* @param s the string.
*/
private static void showAbbreviated( String s )
{
if ( DEBUGGING )
{
out.println( "length:" + s.length() );
if ( s.length() > 600 )
{
out.println( s.substring( 0, 300 ) );
out.println( " ..." );
out.println( " " + s.substring( s.length() - 300 ) );
}
else
{
out.println( s );
}
}
} // /method
/**
* Sort items
*
* @param toSort Arralist of Strings to sort
* @param comparator Comparator to use in Sort
*
* @return true if sort disturbed the order
*/
private static boolean sortItems( ArrayList toSort, Comparator comparator )
{
// empty items already removed.
String[] before = toSort.toArray( new String[ toSort.size() ] );
// default comparable null gets default sensitive order
Collections.sort( toSort, comparator );
String[] after = toSort.toArray( new String[ toSort.size() ] );
for ( int i = 0; i < before.length; i++ )
{
// The sort is reputedly stable so two identical records should not exchange order.
// That would make it safe to compare on identity.
// However that freaks code analysers
if ( !before[ i ].equals( after[ i ] ) )
{
return true;
}
}
return false;
}
/**
* @param args [-v] [-start=xxx] [-end=xxx] -marker=xxx [-comparator=xxx] [-s] dirs/files...
*
* @throws IOException
*/
public static void main( final String[] args ) throws IOException
{
String startMarker = null;
String endMarker = null;
String marker = "";
Comparator comparator = null; // Sensitive
String comparatorName = "Sensitive";
for ( int i = 0; i < args.length; i++ )
{
final String arg = args[ i ];
final int equalPlace = arg.indexOf( '=' );
if ( arg.startsWith( "-" ) && equalPlace >= 0 )
{
final String key = arg.substring( 1, equalPlace ).trim();
final String value = arg.substring( equalPlace + 1 ).trim();
// mark this arg handled so it will not fool the command line processor
args[ i ] = null;
// command line processor has already stripped " " out of parm.
switch ( key )
{
case "start":
startMarker = value;
break;
case "end":
endMarker = value;
break;
case "marker":
marker = value;
break;
case "comparator":
comparatorName = value;
comparator = analyseComparator( comparatorName );
break;
case "q":
case "v":
case "s": /* ignore, part of files */
break;
default:
throw new IllegalArgumentException( "unrecognised option " + arg + "\n" + USAGE );
}
}
else
{
// filenames. CommandLine will deal with it later
}
} // end for
// all have default, except marker
if ( ST.isEmpty( marker ) )
{
throw new IllegalArgumentException( "-marker=\"xxx\" option is missing. The leading - is mandatory.\n" + USAGE );
}
CommandLine commandLine = new CommandLine( args,
new AllButSVNDirectoriesFilter(),
new ExtensionListFilter( ExtensionListFilter.COMMON_TEXT_EXTENSIONS ) );
final boolean verbose = commandLine.isVerbose();
if ( commandLine.size() == 0 )
{
throw new IllegalArgumentException( "No files found to process\n" + USAGE );
}
for ( File file : commandLine )
{
sortOneFile( file, startMarker, marker, endMarker, comparator, comparatorName, verbose );
}
} // /method
/**
* sort items in one String
*
* @param contents String to process
* @param startMarker markes start of itmes to sort
* @param marker separates items to sort
* @param endMarker marks end of items to sort
* @param comparator Comparator for sort
* @param comparatorName Name of Comparator as specified on command line.
* @param whatIsBeingSorted description of what we are sorting, eg. file name, for error messages.
* @param verbose if true, more verbose commentary about the sort
*/
public static String sortContents( String contents,
final String startMarker,
final String marker,
final String endMarker,
final Comparator comparator,
final String comparatorName,
final String whatIsBeingSorted,
boolean verbose ) throws IOException
{
final String head;
// deal with the non-sortable header.
int start;
if ( ST.isEmpty( startMarker ) )
{
// no startMarker, we sort the entire file.
head = "";
start = 0;
}
else
{
start = contents.indexOf( startMarker );
if ( start < 0 )
{
// there are no items to sort in this file.
if ( verbose )
{
out.println( whatIsBeingSorted + " contains nothing to sort." );
}
return null;
}
else
{
start += startMarker.length();
// inlude startmarker in head
head = contents.substring( 0, start );
}
}
/** deal with the non-sortable tail.
tail end of file after sortables
* including the end marker
*/
final String tail;
int end;
if ( ST.isEmpty( endMarker ) )
{
// no endMarker, we sort the entire file.
tail = "";
end = contents.length();
}
else
{
end = contents.lastIndexOf( endMarker );
if ( end < 0 )
{
throw new IOException( "Missing end marker \"" + endMarker + "\" in " + whatIsBeingSorted );
}
else
{
// include marker
tail = contents.substring( end );
}
}
final String guts = contents.substring( start, end );
if ( DEBUGGING )
{
out.println( " H E A D " );
showAbbreviated( head );
out.println( "G U T S" );
showAbbreviated( guts );
out.println( " T A I L " );
showAbbreviated( tail );
}
// figure out how many items there will be to sort.
int expectedItems = 0;
int place = 0;
// count how many items there will be
while ( ( place = contents.indexOf( marker, place ) ) >= 0 )
{
expectedItems++;
place += marker.length();
}
ArrayList toSort = new ArrayList<>( expectedItems );
/** top part of file, prior to sortables
* including the start marker
*/
// parse out the chunks
int startOfChunk = 0;
dropped = 0;
while ( startOfChunk < guts.length() )
{
int endOfChunk = guts.indexOf( marker, startOfChunk );
if ( endOfChunk < 0 )
{
endOfChunk = guts.length();
}
// strip off marker
final String item = guts.substring( startOfChunk, endOfChunk ).trim();
// add item if non-empty
addItem( toSort, item );
startOfChunk = endOfChunk + marker.length();
}
if ( toSort.size() == 0 )
{
if ( verbose )
{
out.println( whatIsBeingSorted + " contains nothing to sort." );
}
return null;
}
boolean disturbedOrder = sortItems( toSort, comparator );
if ( !disturbedOrder && dropped <= 1 )
{
// We did not disturb anything. This is the normal case.
// No need to compose new contents or write back
// expect one empty elt at the end
if ( verbose )
{
out.println( whatIsBeingSorted + " " + toSort.size() + " items already sorted." );
}
return null;
}
// Glue chunks back together with terminating marker and extra blank line between each.
final FastCat sb = new FastCat( toSort.size() * 3 + 4 );
if ( head.length() > 0 )
{
sb.append( head, "\n\n" );
}
for ( String item : toSort )
{
// put markers back in canonical form. e.g. comma back on end of declaration
// We put a marker even after the last item. It is a terminator, not a separator.
// Though it if is missing, all still works.
sb.append( item, marker, "\n\n" );
}
if ( tail.length() > 0 )
{
sb.append( tail, "\n" );
}
out.println( whatIsBeingSorted + " " + toSort.size() + " items sorted with " + comparatorName );
return sb.toString();
}
/**
* sort items in one file
*
* @param file file to process
* @param startMarker markes start of itmes to sort
* @param marker separates items to sort
* @param endMarker marks end of items to sort
* @param comparator Comparator for sort
* @param comparatorName Name of Comparator as specified on command line.
* @param verbose true if verbose commentary about sort
*/
public static void sortOneFile( final File file,
final String startMarker,
final String marker,
final String endMarker,
final Comparator comparator,
final String comparatorName,
final boolean verbose )
throws IOException
{
final String contents = HunkIO.readEntireFile( file, HunkIO.UTF8 );
final String sorted = sortContents( contents, startMarker, marker, endMarker, comparator, comparatorName, "File " + EIO.getCanOrAbsPath( file ), verbose );
// null result means we could not process the contents. Leave as it was.
if ( sorted != null && !sorted.equals( contents ) )
{
HunkIO.writeEntireFile( file, sorted, HunkIO.UTF8 );
}
}
}