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