//
// @(#)MultiOrderEntry.java 1.00 4/1/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.gui.dialog;
import dip.order.OrderException;
import dip.misc.Utils;
import dip.misc.Log;
import dip.gui.ClientFrame;
import dip.gui.OrderDisplayPanel;
import dip.world.Map;
import dip.world.World;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javax.swing.JScrollPane;
/**
*
* Modal dialog for entering multiple orders
* Returns exceptions as a group, HTML-formatted.
* <p>
* This has some special feature to make parsing even easier and
* more reliable.
*
*/
public class MultiOrderEntry
{
// i18n constants
private static final String TITLE = "MOED.title";
private static final String HEADER_TEXT_LOCATION = "MOED.header.text.location";
private static final String RESULT_DIALOG_TITLE = "MOED.dlg.result.title";
private static final String RESULT_DIALOG_HEADER = "MOED.dlg.result.text.result";
private static final String COMMENT_PREFIX = "(*";
// constants
/**
* Eliminates list prefixes, such as:
* (where X = alphanumeric +/- surrounded by whitespace)
* X: <br>
* X. <br>
* X> <br>
* X) <br>
* X] <br>
* (X) <br>
* <X> <br>
* [X] <br>
* and all of above may have a "." or ":" after them as well
* e.g.: X): or X). or 1).
*
*/
private static final String LIST_REGEX = "^\\s*[<\\(\\[]?\\s*\\p{Alnum}*\\s*[>\\)\\]\\.\\:][\\.\\:]?\\s*";
// NOTE: this is sort of a hack.
private static final String[] BAD_TOKS =
{
"army","fleet","wing","a","f","w",
"s", "support", "supports", "sup", "suppor", "supp", "sprt", "supprt", "spprt", "supporting", "supportng",
"convoy", "con", "conv", "convy", "c", "convoying", "convying"
};
// instance variables
private ClientFrame parent;
private TextViewer tv;
private OrderDisplayPanel orderDisplayPanel;
private World world;
private Pattern listPattern = null;
/** Display the MultiOrderEntry dialog */
public static void displayDialog(ClientFrame parent, World world)
{
MultiOrderEntry moe = new MultiOrderEntry(parent, world);
moe.tv.displayDialog();
}// displayDialog()
private MultiOrderEntry(ClientFrame parent, World world)
{
this.parent = parent;
this.world = world;
this.orderDisplayPanel = parent.getOrderDisplayPanel();
tv = new TextViewer(parent, true);
tv.setEditable(true);
tv.setHeaderText( Utils.getText(Utils.getLocalString(HEADER_TEXT_LOCATION)) );
tv.setTitle(Utils.getLocalString(TITLE));
tv.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
tv.addTwoButtons( tv.makeCancelButton(), tv.makeAcceptButton(), false, true );
tv.setAcceptListener(new Acceptor());
tv.setText("");
tv.setHelpID(dip.misc.Help.HelpID.Dialog_MultiOrder);
}// MultiOrderEntry()
private class Acceptor implements TextViewer.AcceptListener
{
public boolean isAcceptable(TextViewer t)
{
String text = t.getText();
text = text.trim();
if(!text.equals(""))
{
return process(t.getText());
}
return true;
}// isAcceptable()
public boolean getCloseDialogAfterUnacceptable()
{
return true;
}// getCloseDialogAfterUnacceptable()
}// nested class Acceptor
// process line-by-line
// keep exceptions
// keep tally of total orders / invalid / valid
private boolean process(String text)
{
int nOrders = 0;
List exList = new ArrayList();
List failList = new ArrayList();
try
{
BufferedReader br = new BufferedReader(new StringReader(text));
String line = br.readLine();
while(line != null)
{
try
{
// first trim
line = line.trim();
// now check length, after trimming
// (otherwise, lines with just whitespace will be interpreted as an order)
if(line.length() > 0)
{
nOrders++;
// trim anything after (and including) a "(*" for cut-and-pastes
// from judge output
int idx = line.indexOf(COMMENT_PREFIX);
if(idx > COMMENT_PREFIX.length())
{
line = line.substring(0, idx);
}
parseOrder(line);
}
}
catch(OrderException oe)
{
exList.add(oe.getMessage());
failList.add(line);
}
line = br.readLine();
}
br.close();
}
catch(IOException ioe)
{
throw new IllegalStateException("BufferedReader: internal error?");
}
int nFailed = exList.size();
if(nFailed > 0)
{
String headerText = Utils.getLocalString(RESULT_DIALOG_HEADER,
new Integer(nOrders),
new Integer(nFailed),
new Integer(nOrders - nFailed));
// order text, formatted with CSS
StringBuffer sb = new StringBuffer(4096);
for(int i=0; i<exList.size(); i++)
{
// failed order
sb.append("<div class=\"ind1top05big\"><u>");
sb.append( failList.get(i) );
sb.append("</u></div>");
// description
sb.append("<div class=\"indent2cmbig\">");
sb.append( exList.get(i) );
sb.append("</div>");
}
// the above text, added to the template
String templateText = Utils.getText(Utils.getLocalString("MOED.dlg.template"));
String dialogText = Utils.format(templateText,
new Object[] {headerText, sb.toString()} );
TextViewer rv = new TextViewer(parent, true);
rv.setEditable(false);
rv.setTitle(Utils.getLocalString(RESULT_DIALOG_TITLE));
rv.addSingleButton( rv.makeOKButton() );
rv.setContentType("text/html");
rv.setText(dialogText);
rv.setHeaderVisible(false);
rv.displayDialog();
return false;
}
return true;
}// process()
/**
* First, clean up the order by applying the List Eliminator
* regex pattern. If parsing fails after this, attempt parsing
* with the Reducing Recursive Token Elimination Parser.
*
*
*/
private void parseOrder(String input)
throws OrderException
{
Log.println("MOE::parseOrder() applying list prefix eliminator on: ", input);
if(listPattern == null)
{
listPattern = Pattern.compile(LIST_REGEX);
}
Matcher m = listPattern.matcher(input);
// we only want ONE match
if(m.lookingAt()) // find FIRST match
{
input = input.substring( m.end() );
}
Log.println("MOE::parseOrder(): after list prefix eliminator: ", input);
Log.println("MOE::parseOrder(): now applying recursive elimination parser...");
recursiveParse(input);
}// parseOrder()
/**
* Reducing Recursive Token Elimination Parser
* <p>
* If an order fails parsing, tokenize the order
* and successively remove (from the beginning)
* tokens and attempt parsing, until it parses.
* <p>
* If the order parses, it is added automatically
* to the order list. If it does not, the first
* exception generated is thrown.
* <p>
* Rational: we can cut/paste from email and the
* beginning content (e.g.: "1> gas-par" the "1>"
* would be the first token eliminated) will be
* ignored.
* <p>
* We 'limit' until the first recognized unit
* type name or province name is detected in a token.
* This helps prevent bad support or convoy orders
* from being over-parsed into move orders.
*
*/
private void recursiveParse(String input)
throws OrderException
{
Log.println("MOE::recursiveParse(): ", input);
OrderException firstException = null;
// first pass
try
{
orderDisplayPanel.addOrderRaw(input, true);
Log.println(" MOE::recursiveParse(): success on first pass.");
return;
}
catch(OrderException oe)
{
Log.println(" MOE::recursiveParse(): first pass failed.");
firstException = oe;
}
// tokenize
final String[] tokens = toTokens(input.toLowerCase());
// test
for(int i=0; i<tokens.length; i++)
{
if(isRecognized(tokens[i]))
{
Log.println(" MOE::recursiveParse(): abort; token is recognized: ", tokens[i]);
break;
}
String text = fromTokens(tokens, i, tokens.length);
Log.println(" MOE::recursiveParse(): now trying: \"", text, "\"");
try
{
orderDisplayPanel.addOrderRaw(text, true);
return;
}
catch(OrderException oe)
{
Log.println(" MOE::recursiveParse(): try failed.");
// do nothing.
}
// if we failed, and this current token already is a
// definate known province, we shouldn't process further.
if(world.getMap().getProvince(tokens[i]) != null)
{
Log.println(" MOE::recursiveParse(): abort; known province recognized: ", tokens[i]);
break;
}
}
throw firstException;
}// recursiveParse()
/** Converts input to token array */
private String[] toTokens(String input)
{
ArrayList list = new ArrayList(10);
StringTokenizer st = new StringTokenizer(input);
while(st.hasMoreTokens())
{
list.add( st.nextToken() );
}
return (String[]) list.toArray(new String[list.size()]);
}// toTokens()
/**
* Converts input array to space-separated String,
* using the given starting and ending indices
* Start is inclusive; End is exclusive
*/
private String fromTokens(String[] tokens, int start, int end)
{
if(start > end || start < 0 || end > tokens.length)
{
throw new IllegalArgumentException();
}
StringBuffer sb = new StringBuffer(256);
for(int i=start; i<end; i++)
{
sb.append(tokens[i]);
sb.append(' ');
}
return sb.toString();
}// fromTokens()
/**
* See if we recognize a Token
* as a:
* a) Power
* b) Unit
*/
private boolean isRecognized(String tok)
{
Map map = world.getMap();
// check against known powers
if(map.getPower(tok) != null)
{
return true;
}
// check against against other bad tokens
for(int i=0; i<BAD_TOKS.length; i++)
{
if(tok.equals(BAD_TOKS[i]))
{
return true;
}
}
return false;
}// isRecognized()
}// class MultiOrderEntry()