/*
* [SortDeclarations.java]
*
* Summary: compare two Java declarations, possibly with lead comment, with trailing ;.
*
* 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.*;
/**
* compare two Java declarations, possibly with lead comment, with trailing ;.
*
* For this to work, keywords much be in canonical order i.e.
* private static final int x;
* Must use separator ;
* Cannot handle static init.
* Cannot handle classes or methods.
* Sorts declarations: public before private, static before instance, final before non-final,
* boolean, Boolean, char, Character, short, Short, int, Integer, long, Long, float, Float,
* double, Double, variable name case-insensitive ascending
*
* @author Roedy Green, Canadian Mind Products
* @version 1.1 2014-05-02 change sort order
* @see com.mindprod.sortcode.SortMethods
* @since 2014-04-30
*/
public class SortDeclarations implements Comparator
{
// todo: ignore annotations.
/**
* how much space for the precomputed keys
*/
private static final int ALLOC_FOR_PRECOMPUTE = 1000;
/**
* 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;
/**
* parse Java declaration
* keywords that apply to declarations
*
* @annotations (ignored)
* public | protected | private ]
* static
* final
* [ transient | volatile ]
* [ int | long | String etc.
*/
private static final String ANNOTATION = "(?:@[\\w]+\\s*(?:\\(\\s*[ \"\\w\\{\\}]+\\s*\\))?\\s*)*";
private static final Pattern DCL_GRABBER = Pattern.compile(
ANNOTATION
+ "(private|public|protected)?\\s*"
+ "(static)?\\s*"
+ "(final)?\\s*"
+ "(transient|volatile)?\\s*"
+ "(boolean|byte|char|int|long|float|double|[A-Za-z0-9_\\.]*)\\s*"
+ "(?:[\\[<][A-Za-z0-9_ ,]*[>\\]])?\\s*" // generics<> array []
+ "([A-Za-z0-9_]+)"
); // var 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 );
private static char calcFinalityOrder( final String finality )
{
if ( ST.isEmpty( finality ) )
{
return '1';
}
switch ( finality )
{
case "final":
return '0';
case "": /* not final */
return '1';
default:
err.println( "unrecognised or misplaced final " + finality );
return 'z';
}
} // /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 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
private static char calcTypeOrder( final String typeName )
{
if ( ST.isEmpty( typeName ) )
{
return 'z';
}
switch ( typeName )
{
case "":
return 'z';
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 );
}
return 'I';
} // end switch
} // /method
private static char calcVarOrder( final String varName, final boolean isStatic, final boolean isFinal )
{
if ( ST.isEmpty( varName ) )
{
return 'z';
}
if ( varName.toUpperCase().equals( varName ) )
{
// all upper caps
// should be static final
if ( !( isStatic && isFinal ) )
{
err.println( "All caps should static final" + varName );
}
return '0';
}
else if ( Character.isUpperCase( varName.charAt( 0 ) ) )
{
// first letter is cap
err.println( "Non-static final variable name should start with lower case letter " + varName );
return '3';
}
else
{
// first letter is lower case, as it should be.
return '3';
}
} // /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 )
{
// whole thing, not just first line, without comment
out.println( clean );
}
final Matcher m = DCL_GRABBER.matcher( clean );
final String[] computedKey;
if ( m.lookingAt() )
{
/**
* parse Java declaration
* keywords that apply to declarations
*
* @Nullable
* public | protected | private ]
* static
* final
* [ transient | volatile ]
* [ int | long | String etc.
*/
int g = 1;
final String scope = ST.canonical( m.group( g++ ) );
final String instance = ST.canonical( m.group( g++ ) );
final String finality = ST.canonical( m.group( g++ ) );
final String trans = ST.canonical( m.group( g++ ) );// transient/volatile)
final String typeName = ST.canonical( m.group( g++ ) );
final String varName = ST.canonical( m.group( g++ ) );
if ( DEBUGGING )
{
out.println( "[scope:" + scope
+ "|static:" + instance
+ "|final:" + finality
+ "|trans:" + trans
+ "|type:" + typeName
+ "|var:" + varName
+ "]" );
}
// analyse keywords we found
final char scopeLetter = calcScopeOrder( scope ); /* public first */
final char instanceOrder = calcInstanceOrder( instance ); /* static first */
final char finalityOrder = calcFinalityOrder( finality ); /* final first */
final char typeOrder = calcTypeOrder( typeName ); /* boolean before int before String before Pattern */
final boolean isStatic = "static".equals( instance );
final boolean isFinal = "final".equals( finality );
final char varOrder = calcVarOrder( varName, isStatic, isFinal ); /* all upper first */
// sort by scope, instance, finality, primitive/class , classname, varname
// glue sort category letter together.
final FastCat sb = new FastCat( 5 );
sb.append( scopeLetter );
sb.append( instanceOrder );
sb.append( finalityOrder ); // for methods, name in more important than type
sb.append( typeOrder );
final String condensedKey = sb.toString();
if ( CASE_SENSITIVE )
{
// case-sensitive version
computedKey = new String[] { condensedKey,
typeName,
String.valueOf( varOrder ),
varName };
}
else
{
// case-insensitive version
computedKey = new String[] { condensedKey,
typeName.toUpperCase(),
String.valueOf( varOrder ),
varName.toUpperCase() };
}
if ( DEBUGGING )
{
for ( String chunk : computedKey )
{
out.print( chunk + "|" );
}
out.println();
}
}
else
{
err.println( "SortDeclarations parse failed." );
err.println( clean );
err.println( "--------------------------------------------------------------" );
computedKey = new String[] { "zzzz", "", "z", clean };
}
// save it in case we need it again
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;
assert extracta != null : "null extracta";
assert extractb != null : "null extractb";
assert extracta.length == extractb.length : "extract array length mismatch";
for ( int i = 0; i < extracta.length; i++ )
{
diff = extracta[ i ].compareTo( extractb[ i ] );
if ( diff != 0 )
{
return diff;
}
}
return 0;
} // /method
} // /methods