package net.vhati.modmanager.core; import java.io.IOException; import java.util.Collections; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A list of messages, with a boolean outcome. */ public class Report { public final List<ReportMessage> messages; public final boolean outcome; public Report( List<ReportMessage> messages, boolean outcome ) { this.outcome = outcome; List<ReportMessage> tmpList = new ArrayList<ReportMessage>( messages ); this.messages = Collections.unmodifiableList( tmpList ); } /** * Formats ReportMessages. * * Symbols are prepended to indicate type. * * Nested messages are indented. * * The Appendable interface claims to throw * IOException, but StringBuffer and StringBuilder * never do. So extra methods specifically accept * those classes and swallow the exception. * * If exceptions are desired, cast args to the * more general type. */ public static class ReportFormatter { protected Pattern breakPtn = Pattern.compile( "(^|\n)(?=[^\n])" ); public String getIndent() { return " "; } public String getPrefix( int messageType ) { switch ( messageType ) { case ReportMessage.WARNING: return "~ "; case ReportMessage.ERROR: return "! "; case ReportMessage.EXCEPTION: return "! "; case ReportMessage.SECTION : return "@ "; case ReportMessage.SUBSECTION: return "> "; case ReportMessage.WARNING_SUBSECTION: return "~ "; case ReportMessage.ERROR_SUBSECTION: return "! "; default: return getIndent(); } } /** * Returns the given CharSequence, or a new one decorated for the type. */ public CharSequence getDecoratedText( int messageType, CharSequence text ) { // Sections get underlined. if ( messageType == ReportMessage.SECTION ) { StringBuilder buf = new StringBuilder( text.length()*2+1 ); buf.append( text ).append( "\n" ); buf.append( getPrefix( messageType ).replaceAll( "\\S", " " ) ); for ( int i=0; i < text.length(); i++ ) buf.append( "-" ); return buf; } else { return text; } } /** * Formats a list of messages. * * Leading newlines in the first message will be omitted. */ public void format( List<ReportMessage> messages, Appendable buf, int indentCount ) throws IOException { for ( int i=0; i < messages.size(); i++ ) { ReportMessage message = messages.get( i ); if ( i == 0 ) { // Skip leading newlines in first message. int start = 0; while ( start < message.text.length() && message.text.charAt(start) == '\n' ) start++; if ( start > 0 ) { // Create a substitute message without them. CharSequence newText = message.text.subSequence( start, message.text.length() ); message = new ReportMessage( message.type, newText, message.nestedMessages ); } } format( message, buf, indentCount ); } } /** * Indents and decorates a message, then formats any nested messages. */ public void format( ReportMessage message, Appendable buf, int indentCount ) throws IOException { // Subsections get an extra linebreak above them. switch ( message.type ) { case ReportMessage.SUBSECTION: case ReportMessage.WARNING_SUBSECTION: case ReportMessage.ERROR_SUBSECTION: buf.append( "\n" ); default: // Not a subsection. } CharSequence seq = getDecoratedText( message.type, message.text ); // Indent the first line. for ( int i=0; i < indentCount; i++ ) buf.append( getIndent() ); buf.append( getPrefix( message.type ) ); // Indent multi-line message text. Matcher m = breakPtn.matcher( seq ); int lastEnd = 0; while ( m.find() ) { if ( m.start() - lastEnd > 0 ) buf.append( seq.subSequence( lastEnd, m.start() ) ); if ( m.group(1).length() > 0 ) { // At \n, instead of 0-length beginning (^). buf.append( "\n" ); for ( int i=0; i < indentCount; i++ ) { buf.append( getIndent() ); } } lastEnd = m.end(); } int srcLen = seq.length(); if ( lastEnd < srcLen ) buf.append( seq.subSequence( lastEnd, srcLen ) ); buf.append( "\n" ); if ( message.nestedMessages != null ) { format( message.nestedMessages, buf, indentCount+1 ); } } /** Exception-swallowing wrapper. */ public void format( List<ReportMessage> messages, StringBuffer buf, int indentCount ) { try { format( messages, (Appendable)buf, indentCount ); } catch( IOException e ) {} } /** Exception-swallowing wrapper. */ public void format( List<ReportMessage> messages, StringBuilder buf, int indentCount ) { try { format( messages, (Appendable)buf, indentCount ); } catch( IOException e ) {} } /** Exception-swallowing wrapper. */ public void format( ReportMessage message, StringBuffer buf, int indentCount ) { try { format( message, (Appendable)buf, indentCount ); } catch( IOException e ) {} } /** Exception-swallowing wrapper. */ public void format( ReportMessage message, StringBuilder buf, int indentCount ) { try { format( message, (Appendable)buf, indentCount ); } catch( IOException e ) {} } } /** * Notice text, with a formatting hint. * * Messages can be compared for equality * to ignore repeats. */ public static class ReportMessage { public static final int INFO = 0; public static final int WARNING = 1; public static final int ERROR = 2; public static final int EXCEPTION = 3; public static final int SECTION = 4; public static final int SUBSECTION = 5; public static final int WARNING_SUBSECTION = 6; public static final int ERROR_SUBSECTION = 7; public final int type; public final CharSequence text; public final List<ReportMessage> nestedMessages; public ReportMessage( int type, CharSequence text ) { this( type, text, new ArrayList<ReportMessage>() ); } public ReportMessage( int type, CharSequence text, List<ReportMessage> nestedMessages ) { this.type = type; this.text = text; List<ReportMessage> tmpList = new ArrayList<ReportMessage>( nestedMessages ); this.nestedMessages = Collections.unmodifiableList( tmpList ); } @Override public boolean equals( Object o ) { if ( o == null ) return false; if ( o == this ) return true; if ( o instanceof ReportMessage == false ) return false; ReportMessage other = (ReportMessage)o; if ( this.type != other.type ) return false; if ( !this.text.equals(other.text) ) return false; if ( this.nestedMessages == null ) { if ( other.nestedMessages != null ) return false; } else { if ( !this.nestedMessages.equals( other.nestedMessages ) ) return false; } return true; } @Override public int hashCode() { int result = 236; int salt = 778; int nullCode = 99; result = salt * result + this.type; result = salt * result + text.hashCode(); if ( this.nestedMessages != null ) result = salt * result + this.nestedMessages.hashCode(); else result = salt * result + nullCode; return result; } } }