// // @(#)OrderFormat.java 4/2002 // // Copyright 2002-2004 Zachary DelProposto. All rights reserved. // Use is subject to license terms. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // Or from http://www.gnu.org/package dip.order.result; // package dip.order; import dip.world.*; import dip.misc.Log; import java.util.*; import java.lang.reflect.*; /** * OrderFormat formats orders according to the specified format string. * <p> * While OrderFormat is more flexible than using Order.toBriefString() or * Order.toFullString(), it is also considerably slower. This is should not * generally be a problem, unless one is doing multiple adjudications * (e.g., for an AI). * <p> * OrderFormat uses keywords that are delimited by braces "{}". Valid keywords * are described below. Any text (including whitespace) not in braces will be * output literally, without modification. * <p> * Example: (Hold order)<br> * <code>{getPower()}: {getSourceUnitType()} {getSource()} {:orderName}</code> * <p> * <ul> * <li><b>{field}</b> inserts the class field, after converting * to a String. If null, an empty String ("") is returned. Fields * need not be public, but, they cannot be within a superclass.</li> * <li><b>{method()}</b> inserts the value returned by a class method, * after converting to a String. If null, an empty String ("") * is returned. Methods must be public, and without paremeters. The * current class and all superclasses are searched.</li> * <li><b>{_keyword_}</b> inserts a defined OrderFormat keyword. These * are also known as "static" keywords, since they are not preceded * by a field or method.</li> * <li><b>{field:modifier}</b> or <b>{method():modifier}</b> handle the * output from the given field or method in a given manner. See * below for defined modifiers.</li> * <li><b>{field:?TRUE:FALSE}</b> or <b>{method():?TRUE:FALSE}</b> handles * the output from a given field or method as a boolean. If the result * is null, or the primitive boolean value <code>false</code>, * the text in "FALSE" is displayed. If non-null or boolean-true, the * text in TRUE may be displayed. Note that the following expressions * {field:?:FALSE} and {field:?TRUE:} are valid; the former will * display an empty value when <code>true</code>, and the latter * an empty value when <code>false</code></li>. Nested statements are * will cause an error. However, keywords may be present in a true * or false clause, but only by themselves. * </ul> * <p> * Keywords (these do not modify fields or methods) * <ul> * <li><b>{_arrow_}</b> displays the Movement arrow, as defined by * OrderFormatOptions.</li> * <li><b>{_orderName_}</b> displays the name of an Order (e.g., "Hold") * </ul> * <p> * Modifiers; these must be preceded by a field or method. * <ul> * <li><b>:showPossesivePower</b> if preceded by a method or field that * is a Power, only displays if OrderFormatOptions allows.</li> * <li><b>:adjective</b> if preceded by a method or field that is a Power, * displays the adjective rather than the noun (e.g., "French" * instead of "France").</li> * <li><b>:coast</b> if preceded by a Location, * returns just the Coast.</li> * <li><b>:province</b> if preceded by a Location, returns just the * Province.</li> * <li><b>:path</b> if preceded by an Array of Locations, or Array of * Provinces, displays the provinces (no coasts) separated by * arrows (as defined by {:arrow}). * </ul> * <p> * If an object is of a type usable by format() (e.g., Location, Coast, * Province, Power, Unit, or OrderName), it will be formatted according to * OrderFormatOptions before being output. If not, it will be converted to * a String (via Object.toString()) before being output. * <p> * Methods are also available to format individual (non-order) components * (such as Provinces, Locations, and Coasts) according to rules defined * by OrderFormatOptions. */ public class OrderFormat { // keywords private final static String ARROW = "_arrow_"; private final static String ORDERNAME = "_orderName_"; // variable-modifying keywords private final static String SHOW_POWER = "showPossesivePower"; private final static String POWER_ADJECTIVE = "adjective"; private final static String LOC_COAST = "coast"; private final static String LOC_PROVINCE = "province"; private final static String PATH = "path"; // all non-modifying keywords private final static String[] ALL_NONMOD_KEYWORDS = { ARROW, ORDERNAME }; // all modifying keywords private final static String[] ALL_MOD_KEYWORDS = { SHOW_POWER, POWER_ADJECTIVE, LOC_COAST, LOC_PROVINCE, PATH }; // misc. constants private final static String EMPTY = ""; private final static String KEYWORD_ERROR = "!keyword_error!"; /** * For null values, when debugging, print the word "null" * followed by the type (indicated by cls). */ private static String handleNull(Class cls) { assert (cls != null); StringBuffer sb = new StringBuffer(64); sb.append("null("); sb.append(cls.getName()); sb.append(")"); return sb.toString(); }// handleNull() /** * Apply style transformations to the given String, * returning a transformed String. */ private static String applyStyle(final int originalStyle, final String input) { if(input == EMPTY) { return input; } // style MUST be a valid OrderFormat STYLE_ constant // final boolean isPlural = ((originalStyle - 10) >= 0); final int style = (isPlural ? (originalStyle - 10) : originalStyle); String text = input; // pluralize, but not if input is empty if(isPlural && input.length() > 0) { text = text+"s"; } switch(style) { case OrderFormatOptions.STYLE_LOWER: text = text.toLowerCase(); break; case OrderFormatOptions.STYLE_UPPER: text = text.toUpperCase(); break; case OrderFormatOptions.STYLE_TITLE: text = toTitleCase(text, false); break; case OrderFormatOptions.STYLE_TITLE_ALL: text = toTitleCase(text, true); break; default: // do nothing } return text; }// applyStyle() /** * Converts the input to Title case. If allWords is true, all * words are converted to Title case, instead of just the first * word. */ private static String toTitleCase(final String input, boolean allWords) { final StringBuffer sb = new StringBuffer(input.length()); boolean isInWord = false; boolean lastState = false; for(int i=0; i<input.length(); i++) { char c = input.charAt(i); isInWord = Character.isLetterOrDigit(c); if(isInWord && lastState != isInWord) { c = Character.toTitleCase(c); } sb.append(c); lastState = isInWord; // if not all words, loop no more. if(!allWords) { sb.append(input.substring(i+1, input.length())); break; } } return sb.toString(); }// toTitleCase() /** * Format a Coast given the order formatting parameters. */ public static String format(OrderFormatOptions ofo, Coast coast) { if(ofo == null) { throw new IllegalArgumentException(); } String text = null; if(coast == null) { text = (ofo.isDebug() ? handleNull(Coast.class) : EMPTY); } else { if(Coast.isDisplayable(coast)) { switch(ofo.getCoastFormat()) { case OrderFormatOptions.FORMAT_BRIEF: text = coast.getAbbreviation(); break; case OrderFormatOptions.FORMAT_FULL: text = coast.getName(); break; case OrderFormatOptions.FORMAT_COAST_PAREN_BRIEF: { StringBuffer sb = new StringBuffer(4); sb.append('('); sb.append(coast.getAbbreviation()); sb.append(')'); text = sb.toString(); } break; case OrderFormatOptions.FORMAT_COAST_PAREN_FULL: { StringBuffer sb = new StringBuffer(16); sb.append('('); sb.append(coast.getName()); sb.append(')'); text = sb.toString(); } break; default: throw new IllegalStateException(); } } else if(ofo.isDebug()) { text = coast.getAbbreviation(); } else { text = EMPTY; } } text = applyStyle(ofo.getCoastStyle(), text); assert (text != null); return text; }// format() /** * Format a Province given the order formatting parameters. */ public static String format(OrderFormatOptions ofo, Province province) { if(ofo == null) { throw new IllegalArgumentException(); } String text = null; if(province == null) { text = (ofo.isDebug() ? handleNull(Province.class) : EMPTY); } else { switch(ofo.getProvinceFormat()) { case OrderFormatOptions.FORMAT_BRIEF: text = province.getShortName(); break; case OrderFormatOptions.FORMAT_FULL: text = province.getFullName(); break; default: throw new IllegalStateException(); } } text = applyStyle(ofo.getProvinceStyle(), text); assert (text != null); return text; }// format() /** * Format a Unit Type given the order formatting parameters. */ public static String format(OrderFormatOptions ofo, Unit.Type unitType) { if(ofo == null) { throw new IllegalArgumentException(); } String text = null; if(unitType == null) { text = (ofo.isDebug() ? handleNull(Unit.Type.class) : EMPTY); } else { switch(ofo.getUnitFormat()) { case OrderFormatOptions.FORMAT_BRIEF: text = unitType.getShortName(); break; case OrderFormatOptions.FORMAT_FULL: text = unitType.getFullName(); break; default: throw new IllegalStateException(); } } text = applyStyle(ofo.getUnitStyle(), text); assert (text != null); return text; }// format() /** * Format a Power given the order formatting parameters. * <p> * <b>Note:</b> FORMAT_BRIEF is not yet supported for Power names. */ public static String format(OrderFormatOptions ofo, Power power) { if(ofo == null) { throw new IllegalArgumentException(); } String text = null; if(power == null) { text = (ofo.isDebug() ? handleNull(Unit.Type.class) : EMPTY); } else { switch(ofo.getPowerFormat()) { case OrderFormatOptions.FORMAT_BRIEF: text = power.getName(); break; case OrderFormatOptions.FORMAT_FULL: text = power.getName(); break; default: throw new IllegalStateException(); } } text = applyStyle(ofo.getPowerStyle(), text); assert (text != null); return text; }// format() /** * Format a Location given the order formatting parameters. */ public static String format(OrderFormatOptions ofo, Location loc) { if(ofo == null) { throw new IllegalArgumentException(); } if(loc == null) { return (ofo.isDebug() ? handleNull(Location.class) : EMPTY); } else { StringBuffer sb = new StringBuffer(64); sb.append( format(ofo, loc.getProvince()) ); final String coastText = format(ofo, loc.getCoast()); if(!EMPTY.equals(coastText)) { sb.append( ofo.getCoastSeparator() ); sb.append( coastText ); } return sb.toString(); } }// format() /** * Formats an Order Name (obtained from an Order) * given the order formatting parameters. */ public static String formatOrderName(OrderFormatOptions ofo, Orderable order) { if(ofo == null) { throw new IllegalArgumentException(); } String text = null; if(order == null) { text = (ofo.isDebug() ? handleNull(Orderable.class) : EMPTY); } else { switch(ofo.getOrderNameFormat()) { case OrderFormatOptions.FORMAT_BRIEF: text = order.getBriefName(); break; case OrderFormatOptions.FORMAT_FULL: text = order.getFullName(); break; default: throw new IllegalStateException(); } } text = applyStyle(ofo.getOrderNameStyle(), text); assert (text != null); return text; }// formatOrderName() /** * Process text within braces. * 1) check if non-modifying keyword * 2) parse, check for method/variable, +/- boolean, +/- modifier * 3) return resultant text */ private static String procBraceText(final OrderFormatOptions ofo, final Orderable order, final String text) { if(text == null) { throw new IllegalArgumentException(); } Object out = null; out = procStaticKeyword(ofo, order, text); if(out == null) { final String[] tokens = text.split(":", 3); if(tokens.length == 0) { Log.println("OrderFormat: cannot parse: {", text, "}"); return EMPTY; } // proc first token out = getViaReflection(order, tokens[0]); // process modifier OR boolean if(tokens.length > 1) { // evaluate boolean expression if(tokens[1].startsWith("?")) { boolean isTrue = false; if(out instanceof Boolean) { isTrue = ((Boolean) out).booleanValue(); } else { isTrue = (out != null); } if(isTrue) { assert (tokens[1].length() > 0); final String tok = tokens[1].substring(1); final Object obj = procStaticKeyword(ofo, order, tok); return (obj == null) ? tok : obj.toString(); } else { if(tokens.length == 2) { // {xxx:?true:} [empty 'false' clause] return EMPTY; } else { assert (tokens.length == 3); final Object obj = procStaticKeyword(ofo, order, tokens[2]); return (obj == null) ? tokens[2] : obj.toString(); } } } else { //process via modifier out = procModKeyword(ofo, order, out, tokens[1]); } } } // process Object into a (formatted) String if(out == null) { return EMPTY; } else if(out instanceof Power) { return format(ofo, (Power) out); } else if(out instanceof Coast) { return format(ofo, (Coast) out); } else if(out instanceof Province) { return format(ofo, (Province) out); } else if(out instanceof Location) { return format(ofo, (Location) out); } else if(out instanceof Unit.Type) { return format(ofo, (Unit.Type) out); } else { // convert object to a String return out.toString(); } }// procBraceText /** * Get the method or field via reflection. Returns null if an * error occured. */ private static Object getViaReflection(final Orderable order, final String name) { assert (order != null); assert (name != null); final Class cls = order.getClass(); final boolean isMethod = (name.endsWith("()")); if(isMethod) { try { return cls.getMethod(name.substring(0, name.length()-2), null).invoke(order, null); } catch(Exception e) { Log.println("OrderFormat::getViaReflection() cannot reflect method \"",name,"\""); Log.println("OrderFormat::getViaReflection() exception details:\n",e); } } else { try { return cls.getDeclaredField(name).get(order); } catch(Exception e) { Log.println("OrderFormat::getViaReflection() cannot reflect field \"",name,"\""); Log.println("OrderFormat::getViaReflection() exception details:\n",e); } } return null; }// getViaReflection() /** * Process a keyword that does NOT require any input. * These are, essentially, constants. */ private static Object procStaticKeyword(final OrderFormatOptions ofo, Orderable order, final String keyWord) { if(keyWord.startsWith("_") && keyWord.endsWith("_")) { if(keyWord.equals(ARROW)) { return ofo.getArrow(); } else if(keyWord.equals(ORDERNAME)) { return formatOrderName(ofo, order); } } return null; }// procStaticKeyword() private static Object procModKeyword(final OrderFormatOptions ofo, final Orderable order, final Object input, final String keyword) { if(keyword.equals(SHOW_POWER)) { if(input == null) { return EMPTY; } else if(input instanceof Power && order != null) { // only show possessive power if it is not the same as the // source power AND we are set to show posessive powers. if( ofo.getShowPossessivePower() && !order.getPower().equals((Power) input) ) { return input; } else { return EMPTY; } } } else if(keyword.equals(POWER_ADJECTIVE)) { if(input instanceof Power) { // get adjective for the power, instead of the power name. // apply power-formatting style String adj = ((Power) input).getAdjective(); return applyStyle(ofo.getPowerStyle(), adj); } } else if(keyword.equals(LOC_COAST)) { if(input instanceof Location) { return ((Location) input).getCoast(); } } else if(keyword.equals(LOC_PROVINCE)) { if(input instanceof Location) { return ((Location) input).getProvince(); } } else if(keyword.equals(PATH)) { if(input == null) { return EMPTY; } else if(input instanceof Location[]) { final StringBuffer sb = new StringBuffer(128); final Location[] locs = (Location[]) input; for(int i=0; i<locs.length; i++) { sb.append( format(ofo, locs[i].getProvince()) ); if(i < (locs.length - 1)) { sb.append(' '); sb.append( ofo.getArrow() ); sb.append(' '); } } return sb.toString(); } else if(input instanceof Province[]) { final StringBuffer sb = new StringBuffer(128); final Province[] provs = (Province[]) input; for(int i=0; i<provs.length; i++) { sb.append( format(ofo, provs[i]) ); if(i < (provs.length - 1)) { sb.append(' '); sb.append( ofo.getArrow() ); sb.append(' '); } } return sb.toString(); } } return KEYWORD_ERROR+keyword; }// procModKeyword() /** * Formats an Order */ public static String format(final OrderFormatOptions ofo, final Orderable order) { if(order == null) { return EMPTY; } return format(ofo, order.getDefaultFormat(), order); }// format() /** * Formats an Order according to the specified order format options, * and the specified order format String */ public static String format(final OrderFormatOptions ofo, final String format, final Orderable order) { if(ofo == null || format == null) { throw new IllegalArgumentException(ofo+","+format); } if(order == null) { return EMPTY; } StringBuffer output = new StringBuffer(256); StringBuffer accum = new StringBuffer(32); boolean inBrace = false; StringTokenizer st = new StringTokenizer(format, "{}", true); while(st.hasMoreTokens()) { String tok = st.nextToken(); if("{".equals(tok) && !inBrace) { inBrace = true; } else if("}".equals(tok) && inBrace) { inBrace = false; output.append( procBraceText(ofo, order, accum.toString()) ); accum = new StringBuffer(); } else { if(inBrace) { accum.append(tok); } else { output.append(tok); } } } if(ofo.getEndWithDot()) { // only append a dot if we think the order is complete; this means // it should not end with a space or arrow. // final String str = output.toString(); if(str.endsWith(" ") || str.endsWith(ofo.getArrow())) { return str; } else { output.append('.'); } } return output.toString(); }// format() /** * Gets an example order, suitable for display in a user interface, * using the given OrderFormatOptions. */ public static String getFormatExample(OrderFormatOptions ofo, OrderFactory of) { // this is about the ONLY time Province or Power objects are // created using 'new' Province prov1 = new Province("Livonia", new String[]{"lvn"}, 0, false); Province prov2 = new Province("St. Petersburg", new String[]{"stp"}, 0, false); Province prov3 = new Province("Golf of Bothnia", new String[]{"gob"}, 0, false); Power power1 = new Power(new String[]{"Russia"}, "Russian", true); Power power2 = new Power(new String[]{"German"}, "German", true); Location src = new Location(prov1, Coast.SEA); Location supSrc = new Location(prov2, Coast.SOUTH); Location supDest = new Location(prov3, Coast.SEA); Support support = of.createSupport(power1, src, Unit.Type.FLEET, supSrc, power2, Unit.Type.FLEET, supDest); return format(ofo, support); }// getFormatExample() }// class OrderFormat