//
// @(#)OrderParser.java 12/2002
//
// Copyright 2002 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 java.util.StringTokenizer;
import java.util.Collection;
import java.util.ArrayList;
import dip.misc.Utils;
import dip.misc.Log;
/**
* Parses text to create an Order object.
* <p>
* NOTE: this code is rather hackish (and in no way reflective of the
* rest of the code base). It is not expandable, or modular. However, it
* is a pretty flexible parser in terms of what it will accept. As a point
* of history, it's the first piece of code written for jDip.
* <p>
* I am gradually replacing the most crusty parts with better code. For example, Coast now
* normalizes coasts with regular expressions, and the code is far cleaner.
* <p>
* In the future, I anticipate we will have some sort of command-normalization (Order.normalize)
* which will be implemented by Order subclasses, and some sort of pattern matching
* that will allow the order to be matched to the tokens. This will allow Order classes to be modfied
* or added without rewriting the OrderParser.
* <p>
* <b>note:</b> The parser is extremely tolerant of misspellings in single-word provinces. However,
* it is not at all tolerant of multiword province misspellings. This is because it cannot easily
* recognize multi-word provinces. A pattern based parser could, and would be more robust in this
* regard.
* <p>
* <pre>
* HOLD:
* <power>: <type> <s-prov> h
*
* MOVE, RETREAT:
* <power>: <type> <s-prov> m <d-prov>
* <power>: m <s-prov> (to) <d-prov>
*
* SUPPORT:
* <power>: <type> <s-prov> s <type> <s-prov>
* <power>: <type> <s-prov> s <type> <s-prov> m <d-prov>
*
* CONVOY:
* <power>: <type> <s-prov> c <type> <s-prov> m <d-prov>
*
* DISBAND:
* <power>: <type> <s-prov> d
*
* BUILD:
* <power>: <build> <type> <s-prov>
*
* REMOVE:
* <power>: <remove> <type> <s-prov>
*
*
* Where:
*
* <type> = "army", "a", "fleet", "f" or <empty>
* <s-prov> = Source province.
* <d-prov> = Destination province.
* <power> = Power name or abbreviation of two or more characters.
* <holds> = "h", "hold", "holds", "stand", "stands".
* <moves> = "-", "->", "=>", "m", "move", "moves", "move to", "moves to".
* <support> = "s", "support", "supports".
* <convoy> = "c", "convoy", "convoys".
* <disband> = "d", "disband".
* <build> = "b", "build"
* <remove> = "r", "remove"
*
* </pre>
*/
public class OrderParser
{
private static OrderParser instance = null;
// il8n constants
private static final String OF_POWER_NOT_RECOGNIZED = "OF_POWER_NOT_RECOGNIZED";
private static final String OF_UNIT_NOT_RECOGNIZED = "OF_UNIT_NOT_RECOGNIZED";
private static final String OF_PROVINCE_NOT_RECOGNIZED = "OF_PROVINCE_NOT_RECOGNIZED";
private static final String OF_PROVINCE_UNCLEAR = "OF_PROVINCE_UNCLEAR";
private static final String OF_NO_UNIT_IN_PROVINCE = "OF_NO_UNIT_IN_PROVINCE";
private static final String OF_TOO_SHORT = "OF_TOO_SHORT";
private static final String OF_INTERNAL_ERROR = "OF_INTERNAL_ERROR";
private static final String OF_UNKNOWN_ORDER = "OF_UNKNOWN_ORDER";
private static final String OF_CONVOY_NO_MOVE_OR_DEST = "OF_CONVOY_NO_MOVE_OR_DEST";
private static final String OF_CONVOY_NO_DEST = "OF_CONVOY_NO_DEST";
private static final String OF_CONVOY_NO_MOVE_SPEC = "OF_CONVOY_NO_MOVE_SPEC";
private static final String OF_SUPPORT_NO_DEST = "OF_SUPPORT_NO_DEST";
private static final String OF_SUPPORT_NO_MOVE = "OF_SUPPORT_NO_MOVE";
private static final String OF_MISSING_DEST = "OF_MISSING_DEST";
private static final String OF_BAD_FOR_POWER = "OF_BAD_FOR_POWER";
private static final String OF_NO_ORDER_TYPE = "OF_NO_ORDER_TYPE";
private static final String OF_POWER_LOCKED = "OF_POWER_LOCKED";
private static final String OF_COAST_INVALID = "OF_COAST_INVALID";
private static final String WHITESPACE = ": \t\n\r";
// the order of replacements is very important!
// all must be in lower case!
private static final String REPLACEMENTS[][] =
{
// misc tiny words that people add
// should NOT include 'to' because to can mean move; it's not always extraneous
// must have spaces before and after
{" in ", " "},
{" an ", " "},
{" of ", " "},
{" on ", " "},
{" is ", " "},
// convert unit-type specifiers
{"fleet", " f "},
{"army", " a "},
{"wing", " w "},
// WAIVE orders. Waive may NOT be abbreviated as "W"; otherwise,
// w xxx (build a wing) may be confused as 'waive'
{"waives"," waive "},
{"waive build "," waive "}, // e.g., waive build [province]; must come before "BUILD"
{"waives build "," waive "},
{"waive builds "," waive "}, // e.g., waive build [province]; must come before "BUILD"
{"waives builds "," waive "},
// adjustment order Remove (since it contains "move", must come before)
{"remove"," r "},
{"removes", " r "},
// for MOVE orders; note that "->" must come before "-"
// also, we MUST replace any "-" in coasts with a "/" first.
{"-=>", " m "},
{"=->", " m "},
{"==>", " m "},
{"-->", " m "},
{"->", " m "},
{"=>", " m "},
{"-", " m "},
{"\u2192", " m "}, // unicode ARROW as used by jDip
{"retreats to ", " m "}, // NOTE: space after "to" to avoid ambiguity e.g., "army bre retreats tol"
{"retreat to ", " m "},
{"retreats", " m "}, // plural first
{"retreat", " m "},
{"moving to ", " m "}, // NOTE: space after "to" ...
{"moves to ", " m "}, // NOTE: space after "to" to avoid ambiguity e.g., "army bre moves tol"
{"moves to ", " m "}, // NOTE: space after "to" to avoid ambiguity e.g., "army bre moves tol"
{"move to ", " m "}, // NOTE: plurals and longer entries MUST come before shorter entries
{"moves", " m "},
{"move", " m "},
{" mv ", " m "}, // for those that like unix
{" attacks on ", " m "}, // we precede the following with a space, since they are nonstandard keywords
{" attacks to ", " m "},
{" attacks into ", " m "},
{" attacks of ", " m "},
{" attack on ", " m "},
{" attack to ", " m "},
{" attack into ", " m "},
{" attack of ", " m "},
{" attacks ", " m "},
{" attack ", " m "},
{" into ", " m "}, // prefixed with space (don't want to get the end of a province)
{" to ", " m "}, // used as a substitute for 'move to'; space prefix here is also important
// SUPPORT orders
{"supports", " s "},
{"support", " s "},
{" to support", " s "}, // prefixed with space (to not get the end of another word)
// HOLD orders
{"holds", " h "},
{"hold", " h "},
{"stands", " h "},
{"stand", " h "},
// CONVOY orders
{"convoys", " c "},
{"convoy", " c "},
{"transports", " c "},
{"transport", " c "},
// DISBAND orders NOTE: 'remove' is up above (before 'move')
{"disbands"," d "},
{"disband"," d "},
// various adjustment orders
{"build"," b "},
// this occurs after coast-normalization, so convert parens to spaces.
{"(", " "},
{")", " "}
};
// DELETION strings for preprocessor; must occur after coast normalization
private static final String TODELETE[] =
{
".", // periods often occur in coast specifiers (e.g., "n.c.")
",", // shouldn't have any commas, but shouldn't be harmful, either.
"\"", // double-quotes filtered out
"\'s", // filter out possesives
"\'", // filter out possesives / single quotes
"(", // parentheses will only get in the way.
")",
};
private OrderParser()
{
}// OrderParser()
/**
* Gets an OrderParser instance
*
*/
public static synchronized OrderParser getInstance()
{
if(instance == null)
{
instance = new OrderParser();
}
return instance;
}// getInstance()
/**
* Parse an order to an Order object.
* <p>
* There are several options to control parsing.
* <pre>
* power:
* "default" power to assume; null if not required
* World:
* current world; needed for province/power matching
*
* locked:
* if true, only orders for the specified power are legal.
* if power==null then an IAE is thrown.
* guess:
* only works if (power==null) and power is NOT locked
* guesses power based upon source province
* Position (derived from world) must be accurate; since guessing depends
* upon knowing the current position information, and phase information.
*
*
* States:
*
* Power Locked Guess Result
* ===== ====== ===== =================================================
* null false false VALID: but power must always be present in text to parse!
* null false true VALID: power is based on source province, whether specified or not
* null true false illegal
* null true true illegal
*
* (defined) false false VALID: power must be specified, if not, assumes "Power" given
* (defined) false true VALID: if power not specified, it is based on source province
* (defined) true false VALID: power *always* is "Power" given
* (defined) true true illegal
* </pre>
*/
public Order parse(OrderFactory orderFactory, String text, Power power, TurnState turnState, boolean locked, boolean guess)
throws OrderException
{
if(orderFactory == null)
{
throw new IllegalArgumentException("null OrderFactory");
}
// check arguments
if(locked && power == null)
{
throw new IllegalArgumentException("power/lock disagreement");
}
if(guess && (power != null || locked || turnState == null))
{
throw new IllegalArgumentException("if guess == true, conditions: turnState != null, power == null, and !locked must all be true");
}
Position position = turnState.getPosition();
Map map = turnState.getWorld().getMap();
String preText = preprocess(text, map);
Log.println("OP: Input:", text);
Log.println("OP: preprocessed:", preText);
return parse(preText, position, map, power, turnState, orderFactory, locked, guess);
}// parse()
/**
* The preprocessor normalizes the orders, converting various order entry
* formats to a single order entry format that is more easily parsed.
*/
private String preprocess(String ord, Map map) throws OrderException
{
// create StringBuffer, after filtering the input string.
// note that this step includes lower-case conversion.
StringBuffer sb = filterInput(ord);
// replace any long (2-word, via space or hyphen) province names
// with shorter version.
// NOTE: this may be overkill, especially since it won't replace
// *partial* province names, like "North-atl"
map.replaceProvinceNames(sb);
// filter out power names [required at beginning to filter out power names
// with odd characters such as hyphens]. Excludes first token.
map.filterPowerNames(sb);
// normalize coasts (Converts to /Xc format)
//Log.println("OP: pre-coast normalization:", sb);
try
{
String ncOrd = Coast.normalize( sb.toString() );
sb.setLength(0);
sb.append(ncOrd);
}
catch(OrderException e)
{
Log.println("OrderException: order: ",sb);
throw new OrderException(Utils.getLocalString(OF_COAST_INVALID, e.getMessage()));
}
//Log.println("OP: post-coast normalization:", sb);
// get the 'power token' (or null).
// this is so if a power name has odd characters in it (e.g., chaos map)
// they do not undergo replacement.
String ptok = map.getFirstPowerToken(sb);
final int startIdx = (ptok == null) ? 0 : ptok.length();
// string replacement
for(int i=0; i<REPLACEMENTS.length; i++)
{
int idx = startIdx;
int start = sb.indexOf(REPLACEMENTS[i][0], idx);
while(start != -1)
{
int end = start + REPLACEMENTS[i][0].length();
sb.replace(start, end, REPLACEMENTS[i][1]);
// repeat search
idx = start + REPLACEMENTS[i][1].length();
start = sb.indexOf(REPLACEMENTS[i][0], idx);
}
}
// delete unwanted characters
delChars(sb, TODELETE);
// re-replace, after conversion
map.replaceProvinceNames(sb);
// filter out power names; often occurs in 'support' orders.
// could also appear in a convoy order as well
// e.g.: France: F gas SUPPORT British F iri HOLD
// or "Britain's" which would be converted to "Britain" by delChars()
// this does NOT filter out the first power name!! (which may be required)
map.filterPowerNames(sb);
return sb.toString();
}// preprocess()
private Order parse(String ord, Position position, Map map, Power defaultPower,
TurnState turnState, OrderFactory orderFactory, boolean locked,
boolean guessing)
throws OrderException
{
// Objects common to ALL order types.
String srcName = null;
String srcUnitTypeName = null;
Power power = defaultPower;
// current token for parsing
String token = null;
StringTokenizer st = new StringTokenizer(ord, WHITESPACE, false);
// Power parsing
// see if first token is a power; if so, parse it
power = map.getFirstPower(ord);
//Log.println("OP:parse(): first token a power? ", power);
// eat up the token (we don't want to reparse it), but
// only if it's NOT null (probably not a power)
if(power != null)
{
getToken(st); // eat token
//String pTok = getToken(st);
//Log.println(" OP:parse(): eating token: ", pTok);
}
// reset power, if null, to default (if specified)
power = (power == null) ? defaultPower : power;
// if we're not allowed to guess, and power is null, error.
if(!guessing && power == null)
{
Log.println("OrderException: order: ", ord);
String pTok = getToken(st);
throw new OrderException(Utils.getLocalString(OF_POWER_NOT_RECOGNIZED, pTok));
}
// if we are locked, the power must be the default power
if(locked)
{
assert(power != null);
if(!power.equals(defaultPower))
{
Log.println("OrderException: order: ", ord);
throw new OrderException(Utils.getLocalString(OF_POWER_LOCKED, defaultPower));
}
}
// NOTE: power may be null at this point, iff guessing==true.
// in this case, it is upto the order-processing logic to parse
// get the correct order from the source region.
// decide if first token is src, src type, or adjustment order
// adjustment orders have a different syntax from other orders
// parse the src type [if any]
//
token = getToken(st);
if(isTypeToken(token))
{
srcUnitTypeName = token;
token = getToken(st);
}
else if(isCommandPrefixed(token))
{
return parseCommandPrefixedOrders(orderFactory, position, map, power, token, st, guessing, turnState);
}
// parse the src province
srcName = token;
// parse the order type -- if this is missing, we
// have a 'defineState' order type
String orderType = null;
if(st.hasMoreTokens())
{
orderType = getToken(st, Utils.getLocalString(OF_NO_ORDER_TYPE));
}
else
{
orderType = "definestate";
}
// create objects for Source, and SourceUnit which
// occur for all orders.
Unit.Type srcUnitType = parseUnitType(srcUnitTypeName);
Location src = parseLocation(map, srcName);
assert(src != null);
assert(srcUnitType != null);
// if we are guessing, guess the power from the source.
// return an error if we cannot..
//
// ALSO, if user specified a power, and "guess" is true,
// and the guessed power != specified, throw an exception.
if(guessing)
{
// getPowerFromLocation() should throw an exception if no unit present.
Power tempPower = getPowerFromLocation(false, position, turnState, src);
if(power != null)
{
if( !tempPower.equals(power) )
{
Log.println("OrderException: order: ", ord);
throw new OrderException(Utils.getLocalString(OF_BAD_FOR_POWER, power));
}
}
power = tempPower;
}
assert(power != null);
// create order based on order type
if(orderType.equals("h"))
{
// HOLD order
// <power>: <type> <s-prov> h
return orderFactory.createHold(power, src, srcUnitType);
}
else if(orderType.equals("m"))
{
return parseMoveOrder(map, turnState, position, orderFactory, st,
power, src, srcUnitType, false);
}
else if(orderType.equals("s"))
{
// SUPPORT order
// <power>: <type> <s-prov> s <type> <s-prov>
// <power>: <type> <s-prov> s <type> <s-prov> [h]
// <power>: <type> <s-prov> s <type> <s-prov> m <d-prov>
//
// get type and/or support source names
TypeAndSource tas = getTypeAndSource(st);
// parse supSrc / supUnit
Unit.Type supUnitType = parseUnitType(tas.type);
Location supSrc = parseLocation(map, tas.src);
assert (supUnitType != null);
assert (supSrc != null);
// get power from unit, if possible
Power supPower = null;
if(position.hasUnit(supSrc.getProvince()))
{
supPower = position.getUnit(supSrc.getProvince()).getPower();
}
// support a MOVE [if specified]
if(st.hasMoreTokens())
{
token = st.nextToken();
if(token.equals("m"))
{
String supDestName = getToken(st, Utils.getLocalString(OF_SUPPORT_NO_DEST));
Location supDest = parseLocation(map, supDestName);
assert(supDest != null);
return orderFactory.createSupport(power, src, srcUnitType,
supSrc, supPower, supUnitType, supDest);
}
else if(!token.equals("h"))
{
// anything BUT a hold is ok.
Log.println("OrderException: order: ", ord);
throw new OrderException(Utils.getLocalString(OF_SUPPORT_NO_MOVE));
}
}
// support a HOLD
return orderFactory.createSupport(power, src, srcUnitType, supSrc,
supPower, supUnitType);
}
else if(orderType.equals("c"))
{
// CONVOY order
// <power>: <type> <s-prov> c <type> <s-prov> m <d-prov>
// get type and/or support source
TypeAndSource tas = getTypeAndSource(st);
String conSrcName = tas.src;
String conUnitName = tas.type;
// verify that there is an "m"
token = getToken(st, Utils.getLocalString(OF_CONVOY_NO_MOVE_OR_DEST));
if(!token.equalsIgnoreCase("m"))
{
Log.println("OrderException: order: ", ord);
throw new OrderException(Utils.getLocalString(OF_CONVOY_NO_MOVE_SPEC));
}
// get the destination
String conDestName = getToken(st, Utils.getLocalString(OF_CONVOY_NO_DEST));
// parse convoy src/dest/type
Location conSrc = parseLocation(map, conSrcName);
Location conDest = parseLocation(map, conDestName);
Unit.Type conUnitType = parseUnitType(conUnitName);
// get power, from unit
Power conPower = null;
if(position.hasUnit(conSrc.getProvince()))
{
conPower = position.getUnit(conSrc.getProvince()).getPower();
}
// create order.
return orderFactory.createConvoy(power, src, srcUnitType,
conSrc, conPower, conUnitType, conDest);
}
else if(orderType.equals("d"))
{
// DISBAND order
return createDisbandOrRemove(orderFactory, turnState, true, power, src, srcUnitType);
}
else if(orderType.equals("definestate"))
{
return orderFactory.createDefineState(power, src, srcUnitType);
}
Log.println("OrderException: order: ", ord);
throw new OrderException(Utils.getLocalString(OF_UNKNOWN_ORDER, orderType));
}// parse
/**
* Parses the "rest" of a move/retreat order; this finds
* the destination location, and the location-list if it is
* a convoyed move. It also checks for the "by convoy" or "via convoy"
* phrase that signfies a convoyed move.
* <p>
* ignoreFirstM will ignore a token called "m" if set to true.
* <p>
* This will return a Move or Retreat order, or throw an OrderException.
*/
private Order parseMoveOrder(Map map, TurnState turnState, Position position,
OrderFactory orderFactory, StringTokenizer st,
Power srcPower, Location srcLoc, Unit.Type srcUnitType,
boolean ignoreFirstM)
throws OrderException
{
// MOVE order, or RETREAT order, if we are in RETREAT phase. If so, we can ignore the convoy stuff.
// <power>: <type> <s-prov> m <d-prov>
//
boolean isExplicitConvoy = false; // "by convoy" or "via convoy" present
boolean isConvoyedMove = false; // multiple 'move' locations
String destName = getToken(st, Utils.getLocalString(OF_MISSING_DEST));
// eat possible first "M" if allowed and repeat dest-getting attempt
if(ignoreFirstM && destName.equals("m"))
{
destName = getToken(st, Utils.getLocalString(OF_MISSING_DEST));
}
//
// We do 2 things in this loop:
// (1) JUDGE compatibility:
// check and see if we have 'multiple' destinations; e.g.,
// "A STP-BAR-NRG-NTH-YOR" we BAR is not the dest, YOR is.
// so we will keep parsing until we come across the last valid
// province. A move specifier ('m') *MUST* occur before each province.
// (2) "via convoy" or "by convoy" checking
//
ArrayList al = null;
if(st.hasMoreTokens())
{
al = new ArrayList();
al.add(srcLoc.getProvince());
// parse first destination (and add to array list)
al.add( parseLocation(map, destName).getProvince() );
}
while(st.hasMoreTokens())
{
String token = st.nextToken();
if(token.equals("m"))
{
destName = getToken(st, Utils.getLocalString(OF_MISSING_DEST));
Location pathLoc = parseLocation(map, destName);
assert(pathLoc != null);
al.add(pathLoc.getProvince());
isConvoyedMove = true;
}
else if((token.equals("via") || token.equals("by")) && st.hasMoreTokens())
{
token = st.nextToken();
if(token.equals("c"))
{
isExplicitConvoy = true; // we are convoying!
}
}
}
// final destination
Location dest = parseLocation(map, destName);
assert(dest != null);
if(turnState.getPhase().getPhaseType() == Phase.PhaseType.RETREAT)
{
return orderFactory.createRetreat(srcPower, srcLoc, srcUnitType, dest);
}
else
{
if(isConvoyedMove) // MUST test this first -- it overrides isExplicitConvoy
{
assert(al != null);
Province[] convoyRoute = (Province[]) al.toArray(new Province[al.size()]);
return orderFactory.createMove(srcPower, srcLoc, srcUnitType, dest, convoyRoute);
}
else if(isExplicitConvoy)
{
return orderFactory.createMove(srcPower, srcLoc, srcUnitType, dest, isExplicitConvoy);
}
else
{
// implicit convoy [determiend by Move.validate()] or nonconvoyed move order
return orderFactory.createMove(srcPower, srcLoc, srcUnitType, dest);
}
}
}// parseMoveOrder()
/**
* Parse command-prefixed orders (e.g., "Build army paris"). This typically
* applies to adjustment orders, however, we also allow Move orders to be
* specified this way.
*
*/
private Order parseCommandPrefixedOrders(OrderFactory orderFactory, Position position,
Map map, Power power, String orderType, StringTokenizer st,
boolean guessing, TurnState turnState)
throws OrderException
{
// these orders have a command-specifier BEFORE unit/src information
if(orderType.equals("waive"))
{
// WAIVE order
// <power>: <waive> <province>
TypeAndSource tas = getTypeAndSource(st); // we ignore 'type', but let it be specified
Location src = parseLocation(map, tas.src);
if(guessing)
{
power = getPowerFromLocation(true, position, turnState, src);
}
return orderFactory.createWaive(power, src);
}
else if(orderType.equals("b"))
{
// BUILD order
// <power>: BUILD <type> <s-prov>
TypeAndSource tas = getTypeAndSource(st);
Location src = parseLocation(map, tas.src);
Unit.Type unitType = parseUnitType(tas.type);
if(guessing)
{
power = getPowerFromLocation(true, position, turnState, src);
}
return orderFactory.createBuild(power, src, unitType);
}
else if(orderType.equals("r"))
{
// REMOVE order
// <power>: REMOVE <type> <s-prov>
TypeAndSource tas = getTypeAndSource(st);
Location src = parseLocation(map, tas.src);
Unit.Type unitType = parseUnitType(tas.type);
if(guessing)
{
power = getPowerFromLocation(true, position, turnState, src);
}
return createDisbandOrRemove(orderFactory, turnState, false, power, src, unitType);
}
else if(orderType.equals("m"))
{
// MOVE order: command-first version
// <power>: m <unit> <location> m <location>
// example: "france: move army paris to gascony"
// or: "move army paris-gascony"
TypeAndSource srcTas = getTypeAndSource(st);
Location src = parseLocation(map, srcTas.src);
Unit.Type srcUnitType = parseUnitType(srcTas.type);
if(guessing)
{
power = getPowerFromLocation(true, position, turnState, src);
}
return parseMoveOrder(map, turnState, position, orderFactory, st,
power, src, srcUnitType, true);
}
else if(orderType.equals("d"))
{
// DISBAND: command-first version
// <power>: DISBAND <type> <s-prov>
TypeAndSource tas = getTypeAndSource(st);
Location src = parseLocation(map, tas.src);
Unit.Type unitType = parseUnitType(tas.type);
if(guessing)
{
power = getPowerFromLocation(true, position, turnState, src);
}
return createDisbandOrRemove(orderFactory, turnState, true, power, src, unitType);
}
throw new IllegalArgumentException(Utils.getLocalString(OF_INTERNAL_ERROR, orderType));
}// parseCommandPrefixedOrders()
private String getToken(StringTokenizer st, String error) throws OrderException
{
if(st.hasMoreTokens())
{
return st.nextToken();
}
else
{
throw new OrderException(error);
}
}// getToken()
private String getToken(StringTokenizer st) throws OrderException
{
return getToken(st, Utils.getLocalString(OF_TOO_SHORT));
}// getToken()}
/**
* Derives the power based upon the location of the source unit. We have a
* special flag (isAdjToken) which should be set to TRUE if we are parsing
* an adjustment-phase order, and false otherwise.
*
*/
private Power getPowerFromLocation(boolean isAdjToken, Position position, TurnState turnState, Location source)
throws OrderException
{
Province province = source.getProvince();
Phase phase = turnState.getPhase();
if(phase.getPhaseType() == Phase.PhaseType.ADJUSTMENT && isAdjToken)
{
// adjustment phase
// we are supposed to 'guess', so we guess by getting the owner of the supply center chosen.
// NOTE: this is loose (supply center, rather than home supply center); validation will take
// care of details.
//
// if a unit exists, assume remove, and use that power; otherwise, assume a build.
//
if(position.hasUnit(province))
{
return position.getUnit(province).getPower();
}
else
{
assert(position.getSupplyCenterOwner(province) != null);
return position.getSupplyCenterOwner(province);
}
}
else
{
// retreat / movement phases:
Unit unit = (phase.getPhaseType() == Phase.PhaseType.RETREAT) ? position.getDislodgedUnit(province) : position.getUnit(province);
if(unit != null)
{
return unit.getPower();
}
}
throw new OrderException(Utils.getLocalString(OF_NO_UNIT_IN_PROVINCE, province));
}// getPowerFromLocation()
/** Determine if a Token is a Unit.Type token */
private boolean isTypeToken(String s)
{
if(s.equals("f") || s.equals("a") || s.equals("w"))
{
return true;
}
return false;
}// isTypeToken
// deletes any strings in the stringBuffer that match
// strings specified in toDelete
private void delChars(StringBuffer sb, String[] toDelete)
{
for(int i=0; i<toDelete.length; i++)
{
int idx = sb.indexOf(toDelete[i]);
while(idx != -1)
{
sb.delete(idx, idx + toDelete[i].length());
idx = sb.indexOf(toDelete[i], idx);
}
}
}// delChars()
/**
* Filters out any ISO control characters; improves the
* robustness of pasted text parsing. Also replaces any
* whitespace with a true space character. Returns a new
* StringBuffer.
* <p>
* Also trims and lowercases the input, too
*/
private StringBuffer filterInput(String input)
{
input = input.trim();
StringBuffer sb = new StringBuffer(input.length());
// delete control chars and whitespace conversion
for(int i=0; i<input.length(); i++)
{
final char c = input.charAt(i);
if( Character.isWhitespace(c) )
{
sb.append(' ');
}
else if( !Character.isIdentifierIgnorable(c) )
{
sb.append( Character.toLowerCase(c) );
}
}
return sb;
}// delChars()
/**
* Some orders have the verb (command) at the beginning; e.g.:
* "Build army france". We also allow move orders
* to be specified this way, but most commonly
* adjustment orders are specified this way.
*
*/
private boolean isCommandPrefixed(String s)
{
// b,r,w = build, remove, waive
// m = move
if( s.equalsIgnoreCase("b")
|| s.equalsIgnoreCase("r")
|| s.equalsIgnoreCase("d")
|| s.equalsIgnoreCase("m")
|| s.equalsIgnoreCase("waive") )
{
return true;
}
return false;
}// isCommandPrefixed
private TypeAndSource getTypeAndSource(StringTokenizer st) throws OrderException
{
// given a StringTokenize, parse the next token
// to determine if it is a type (Army or Fleet).
// if it is missing, sets token to null, and sets
// source token.
TypeAndSource tas = new TypeAndSource();
String token = getToken(st);
if(isTypeToken(token))
{
tas.type = token;
tas.src = getToken(st);
}
else
{
tas.src = token;
}
return tas;
}// getTypeAndSource()
private class TypeAndSource
{
public String type = null;
public String src = null;
}// inner class TypeAndSource
/**
* Parses a Location; never returns a null Location;
* will throw an exception if the Location is unclear
* or not recognized. This is similar to Map.parseLocation()
* except that more detailed error information is returned.
* <p>
* <b>THIS ASSUMES COASTS HAVE ALREADY BEEN NORMALIZED WITH
* Coast.normalize()</b>
*/
private Location parseLocation(Map map, String locName)
throws OrderException
{
// parse the coast
Coast coast = Coast.parse(locName); // will return Coast.UNDEFINED at worst
// parse the province. if there are 'ties', we return the result.
final Collection col = map.getProvincesMatchingClosest(locName);
final Province[] provinces = (Province[]) col.toArray(new Province[col.size()]);
if(provinces.length == 0)
{
// nothing matched! we didn't recognize.
throw new OrderException(Utils.getLocalString(OF_PROVINCE_NOT_RECOGNIZED, locName));
}
else if(provinces.length == 1)
{
return new Location(provinces[0], coast);
}
else if(provinces.length == 2)
{
// 2 matches... means it's unclear!
throw new OrderException(Utils.getLocalString(OF_PROVINCE_UNCLEAR,
locName, provinces[0], provinces[1]));
}
else
{
// multiple matches! unclear. give a more detailed error message.
// create a comma-separated list of all but the last.
StringBuffer sb = new StringBuffer(128);
for(int i=0; i<(provinces.length-1); i++)
{
sb.append(provinces[i]);
sb.append(", ");
}
throw new OrderException(Utils.getLocalString(OF_PROVINCE_UNCLEAR,
locName,
sb.toString(),
provinces[provinces.length - 1]));
}
}// parseLocation()
//
// uses unit.Type.parse()
// f/fleet -> FLEET
// a/army -> ARMY
// w/wing -> WING
// null -> UNDEFINED
// any other -> null
//
private Unit.Type parseUnitType(String unitName) throws OrderException
{
Unit.Type unitType = Unit.Type.parse(unitName);
if(unitType == null)
{
throw new OrderException(Utils.getLocalString(OF_UNIT_NOT_RECOGNIZED, unitName));
}
return unitType;
}// parseUnitType()
private Power parsePower(Map map, String powerName) throws OrderException
{
Power power = map.getPowerMatching(powerName);
if(power == null)
{
throw new OrderException(Utils.getLocalString(OF_POWER_NOT_RECOGNIZED, powerName));
}
return power;
}// parsePower()
/**
* Creates a Disband or Remove order, depending upon the phase.
* Since some people use "Disband" to mean "Remove" and vice-versa,
* but jDip interprets them differently (Disband is for retreat
* phase, Remove is for adjustment phase). If the phase is not
* adjustment or retreat, we create the 'desired' order, so that
* the error message is correct.
*/
private Order createDisbandOrRemove(OrderFactory orderFactory, TurnState ts,
boolean disbandPreferred, Power power, Location src, Unit.Type unitType)
throws OrderException
{
if(ts.getPhase().getPhaseType() == Phase.PhaseType.RETREAT)
{
return orderFactory.createDisband(power, src, unitType);
}
else if(ts.getPhase().getPhaseType() == Phase.PhaseType.ADJUSTMENT)
{
return orderFactory.createRemove(power, src, unitType);
}
if(disbandPreferred)
{
return orderFactory.createDisband(power, src, unitType);
}
else
{
return orderFactory.createRemove(power, src, unitType);
}
}// createDisbandOrRemove()
}// class OrderParser