/*
* [SortMethods.java]
*
* Summary: Sort Methods in order in Java source code.
*
* Copyright: (c) 2014-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 2014-04-30 initial version
* 1.1 2014-05-02 change sort order
*/
package com.mindprod.sortcode;
import com.mindprod.common18.FNV1a64;
import com.mindprod.common18.ST;
import com.mindprod.fastcat.FastCat;
import java.util.Comparator;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.System.*;
/**
* Sort Methods in order in Java source code.
*
* compare two Java methods, possibly with lead JavaDoc comment, with trailing // /method
* For this to work, keywords much be in canonical order i.e.
* private static final int getWeight
* Must use separator [// /method]
* Cannot handle classes or declarations.
*
* Sorts methods: abstract before implemented, public before private, static before instance, native before non-native,
* boolean, Boolean, char, Character, short, Short, int, Integer, long, Long, float, Float,
* double, Double, method name case-insensitive ascending.
*
* @author Roedy Green, Canadian Mind Products
* @version 1.1 2014-05-02 change sort order
* @since 2014-04-30
*/
public class SortMethods implements Comparator
{
// todo: tie breaker using method parms
/**
* true if want extra debugging output
*/
private static final boolean DEBUGGING = false;
/**
* set true if want lower case after upper case.
* set false if want upper and lower case mixed together
*/
private static final boolean CASE_SENSITIVE = false;
/**
* how much space for the precomputed keys
*/
private static final int ALLOC_FOR_PRECOMPUTE = 1000;
private static final String ANNOTATION = "(?:@[\\w]+\\s*(?:\\(\\s*[ \"\\w\\{\\}]+\\s*\\))?\\s*)*";
/**
* parse Java declaration
* keywords that apply to methods
* public | protected | private ]
* abstract
* static
* final
* synchronized
* native
* strictfp
* int | long | String | etc.
*/
private static final Pattern METHOD_GRABBER = Pattern.compile( ANNOTATION
+ "(private|public|protected)?\\s*"
+ "(abstract)?\\s*"
+ "(static)?\\s*"
+ "(final)?\\s*"
+ "(synchronized)?\\s*"
+ "(native)?\\s*"
+ "(strictfp)?\\s*"
+ "(void|boolean|byte|char|int|long|float|double|[A-Za-z0-9_\\.]*)\\s*"
+ "(?:[\\[<][A-Za-z0-9_ ,]*[>\\]])?\\s*" // generics<> array []
+ "([A-Za-z0-9_]+)" ); // method name
/**
* there is no point in caching segments of code that one one else is still using.
* WeakHashMap gets rid of keys prematurely because noone else but us ever sees
* the chunks. When we stop looking at them, WeakHashMap drops them too. The sort never
* sees the chunks. We don't want chunks hanging around, so we digest with a hash.
*/
private static HashMap precomputedKeys = new HashMap<>( ALLOC_FOR_PRECOMPUTE );
/**
* put abstract first, part of contract with outside world.
*
* @param abstr "abstract" or ""
*
* @return sort order
*/
private static char calcAbstrOrder( final String abstr )
{
if ( ST.isEmpty( abstr ) )
{
return '1';
}
else
{
return '0';
}
} // /method
private static char calcInstanceOrder( final String instance )
{
if ( ST.isEmpty( instance ) )
{
return '1';
}
switch ( instance )
{
case "static":
return '0';
case "": /*instance */
return '1';
default:
err.println( "unrecognised or misplaced static/instance " + instance );
return 'z';
}
} // /method
private static char calcMainOrder( final String methodName, final String context )
{
if ( ST.isEmpty( methodName ) )
{
return 'z';
}
// put main at the end.
else if ( methodName.equals( "main" ) )
{
return '2';
}
else
{
if ( Character.isUpperCase( methodName.charAt( 0 ) ) )
{
// first letter is cap
err.println( "method name name should start with lower case letter " + methodName );
err.println( context );
}
return '1';
}
} // /method
private static char calcNativityOrder( final String nativity )
{
if ( ST.isEmpty( nativity ) )
{
return '1';
}
else
{
return '0';
}
} // /method
private static char calcScopeOrder( final String scope )
{
if ( ST.isEmpty( scope ) )
{
return '1';
}
switch ( scope )
{
case "public":
return '0';
case "": /* package default */
return '1';
case "protected":
return '2';
case "private":
return '3';
default:
err.println( "unrecognised or misplaced scope " + scope );
return 'z';
}
} // /method
/**
* calculate ordering letter based on the type name
*
* @param typeName name of typue e.g. int Integer HashMap
* @param context itemfor error message
*
* @return latter giving priority order based on type
*/
private static char calcTypeOrder( final String typeName, final String context )
{
if ( ST.isEmpty( typeName ) )
{
return 'z';
}
switch ( typeName )
{
case "":
return 'z';
case "void":
return '0';
case "boolean":
return '1';
case "Boolean":
return '2';
case "byte":
return '3';
case "Byte":
return '4';
case "char":
return '5';
case "Character":
return '6';
case "short":
return '7';
case "Short":
return '8';
case "int":
return '9';
case "Integer":
return 'A';
case "long":
return 'B';
case "Long":
return 'C';
case "float":
return 'D';
case "Float":
return 'E';
case "double":
return 'F';
case "Double":
return 'G';
case "String":
return 'H';
default:
// we should have a class name of some sort e.g. Double, HashMap
if ( Character.isLowerCase( typeName.charAt( 0 ) ) && !typeName.contains( "." ) )
{
err.println( "Class name should start with a capital letter: " + typeName );
err.println( context );
}
return 'I';
} // end switch
} // /method
private static String[] computeKey( String s )
{
// use regex to pluck info useful for sort.
// The declaration we might parse looks like:
// private static final int q;
// or public String = "abc";
// or private static final ArrayList COLLECTION = new ArrayList<>( COUNT_OF_FILES_TO_COLLECT );
final String clean = stripLeadComment( s );
// see if we have done this before:
long hash = FNV1a64.computeHash( clean );
final String[] precomputedKey = precomputedKeys.get( hash );
if ( precomputedKey != null )
{
return precomputedKey;
}
if ( DEBUGGING )
{
// show just the first line.
int place = clean.indexOf( '\n' );
out.println( clean.substring( 0, place ) );
}
final String[] computedKey;
final Matcher m = METHOD_GRABBER.matcher( clean );
if ( m.lookingAt() )
{
/**
* parse Java declaration
* keywords that apply to methods
* public | protected | private ]
* abstract
* static
* final
* synchronized
* native
* strictfp
* int | long | String | etc.
*/
int g = 1;
final String scope = ST.canonical( m.group( g++ ) );
final String abstr = ST.canonical( m.group( g++ ) );
final String instance = ST.canonical( m.group( g++ ) );
final String finality = ST.canonical( m.group( g++ ) );
final String sync = ST.canonical( m.group( g++ ) );
final String nativity = ST.canonical( m.group( g++ ) );
final String strict = ST.canonical( m.group( g++ ) );
final String typeName = ST.canonical( m.group( g++ ) );
final String methodName = ST.canonical( m.group( g++ ) );
if ( DEBUGGING )
{
out.println( "[scope:" + scope
+ "|abstr:" + abstr
+ "|static:" + instance
+ "|final:" + finality
+ "|sync:" + sync
+ "|native:" + nativity
+ "|strict:" + strict
+ "|type:" + typeName
+ "|method:" + methodName
+ "]" );
}
/** analyse the keywords we found */
final char mainOrder = calcMainOrder( methodName, clean ); // main last
final char abstrOrder = calcAbstrOrder( abstr ); // abstract first
final char scopeOrder = calcScopeOrder( scope ); // public first
final char instanceOrder = calcInstanceOrder( instance ); /// static first
final char nativityOrder = calcNativityOrder( nativity ); // native first
final char typeOrder = calcTypeOrder( typeName, clean ); // boolean before int before String
// sort by main, scope, instance, nativity
final FastCat sb = new FastCat( 5 );
sb.append( mainOrder );
sb.append( abstrOrder );
sb.append( scopeOrder );
sb.append( instanceOrder );
sb.append( nativityOrder );
// for declarations, type is more important than name , for methods the reverse.
// typeOrder comes later.
final String condensedKey = sb.toString();
if ( CASE_SENSITIVE )
{
// case-sensitive version
computedKey = new String[] { condensedKey,
methodName,
String.valueOf( typeOrder ),
typeName };
}
else
{
// case-insensitive version
computedKey = new String[] { condensedKey,
methodName.toUpperCase(),
String.valueOf( typeOrder ),
typeName.toUpperCase() }
;
}
if ( DEBUGGING )
{
for ( String chunk : computedKey )
{
out.print( chunk + "|" );
}
out.println();
}
}
else
{
err.println( "SortMethods parse failed." );
// show just the first line.
int place = clean.indexOf( '\n' );
if ( place < 0 )
{
place = Math.min( clean.length(), 200 );
}
err.println( clean.substring( 0, place ) );
err.println( "--------------------------------------------------------------------------------" );
computedKey = new String[] { "zzzzz", clean, "z", "" };
}
// save it in case we need it again without recomputing. It will not be in there already.
precomputedKeys.put( hash, computedKey );
return computedKey;
} // /method
/**
* remove lead comment.
*
* @param s block of code. pretrimmed.
*
* @return block of code with any lead javadoc removed.
*/
private static String stripLeadComment( final String s )
{
// s has already been trimmed
final String clean;
if ( s.startsWith( "/**" ) )
{
final int end = s.indexOf( "*/", "/**".length() );
if ( end < 0 )
{
// malformed comment
err.println( "malformed JavaDoc comment: [-- " + s + " --]\n" );
return s;
}
else
{
return s.substring( end + "*/".length() ).trim();
}
}
else
{
// no comment present.
return s;
}
} // /method
/**
* Compare two String Objects each containing a method body
* Informally, returns (a-b), or +ve if a comes after b.
*
* @param a first String to compare
* @param b second String to compare
*
* @return +ve if a>b, 0 if a==b, -ve if a<b
*/
public final int compare( String a, String b )
{
final String[] extracta = computeKey( a );
final String[] extractb = computeKey( b );
// sort on synthetic key that orders keywords
int diff = 0;
for ( int i = 0; i < extracta.length; i++ )
{
diff = extracta[ i ].compareTo( extractb[ i ] );
if ( diff != 0 )
{
return diff;
}
}
return 0;
} // /method
} // /methods