/* * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved. * * http://izpack.org/ * http://izpack.codehaus.org/ * * Copyright 2002 Elmar Grom * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.izforge.izpack.panels; import com.izforge.izpack.installer.InstallData; import com.izforge.izpack.util.Debug; import com.izforge.izpack.util.VariableSubstitutor; import org.apache.regexp.RE; import javax.swing.*; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.Serializable; import java.util.Map; import java.util.StringTokenizer; import java.util.Vector; /*---------------------------------------------------------------------------*/ /** * This class assists the user in entering serial numbers. <BR> * <BR> * Serial numbers, license number, CD keys and the like are often lenghty alpha-numerical numbers. * In many cases they are devided into multiple parts by dash or point separators. Entering these in * a single text field can be a frustrating experience for the user. This class provides a way of * presenting the user with an assembly of input fields that are arranged in the same way as the * key, with the separators already in place. Immideate testing for format compliance if performed * ans soon as each field is completed. In addition, the cursor is automatically advanced to make * entering numbers as painless as possible. <br> * <br> * <b>Formatting:</b> * <p/> * <ul> * <li><code>N:X:Y </code>- numeric field, accepts digits only * <li><code>H:X:Y </code>- hex field, accepts only hexadecimal digits * <li><code>A:X:Y </code>- alpha field, accepts only letters, no digits * <li><code>AN:X:Y</code>- alpha-numeric field, accepts digits and letters * </ul> * <b>Example:</b> <br> * <br> * <code>"N:4:4 - H:6:6 - AN:3:3 x A:5:5"</code><br> * <br> * This formatting string will produce a serial number field consisting of four separate input * fields. The fisrt input field will accept four numeric digits, the second six hexa-decimal * digits, the third three alpha-numeric digits and the fourth five letters. The first three input * fields will be separated by '-' and the third and fourth by 'x'. The following snapshot was * obtained with this setting: <br> * <br> * <img src="doc-files/RuleInputField-1.gif"/> * * @author Elmar Grom * @version 0.0.1 / 10/19/02 */ /*---------------------------------------------------------------------------*/ public class RuleInputField extends JComponent implements KeyListener, FocusListener, CaretListener, ProcessingClient { /** * */ private static final long serialVersionUID = 3832616275124958257L; /** * Used to specify the retsult format. This constant specifies to return the contents of all * fields concatenated into one long string, with separation between each component. */ public static final int PLAIN_STRING = 1; /** * Used to specify the retsult format. This constant specifies to return the contents of all * fields together with all separators as specified in the field format concatenated into one * long string. In this case the resulting string looks just like the user saw it during data * entry */ public static final int DISPLAY_FORMAT = 2; /** * Used to specify the retsult format. This constant specifies to return the contents of all * fields concatenated into one long string, with a special separator string inserted in between * the individual components. */ public static final int SPECIAL_SEPARATOR = 3; /** * Used to specify the retsult format. This constant specifies to return the contents of all * fields in a somehow modified way. How the content is modified depends on the operation * performed by a encryption service class. The class must be provided at object instatiation. */ public static final int ENCRYPTED = 4; /** * Used internally to identify the default setting for the return format. */ private static int DEFAULT = DISPLAY_FORMAT; private Vector<Serializable> items = new Vector<Serializable>(); /** * This <code>Vector</code> holds a reference to each input field, in the order in which they * appear on the screen. */ private Vector inputFields = new Vector(); private boolean hasParams = false; private Map<String, String> validatorParams; private RuleTextField activeField; private boolean backstep = false; private Toolkit toolkit; private String separator; private int resultFormat = DEFAULT; private InstallData idata = null; /** * Holds an instance of the <code>Validator</code> if one was specified and available */ private Validator validationService; /** * Holds an instance of the <code>Processor</code> if one was specified and available */ private Processor encryptionService; /*--------------------------------------------------------------------------*/ // javadoc inherited public boolean hasParams() { return hasParams; } /*--------------------------------------------------------------------------*/ /** * Constructs a rule input field. * * @param format a string that specifies the formatting and to a limited degree the behavior of * this field. * @param preset a string that specifies preset values for specific sub-fields. * @param separator a string to be used for separating the contents of individual fields in the * string returned by <code>getText()</code> * @param validator A string that specifies a class to perform validation services. The string * must completely identify the class, so that it can be instantiated. The class must implement * the <code>RuleValidator</code> interface. If an attempt to instantiate this class fails, no * validation will be performed. * @param validatorParams A <code>java.util.Map</code> containing name/ value pairs, which * will be forwarded to the validator. * @param processor A string that specifies a class to perform processing services. The string * must completely identify the class, so that it can be instantiated. The class must implement * the <code>Processor</code> interface. If an attempt to instantiate this class fails, no * processing will be performed. Instead, the text is returned in the default formatting. * @param resultFormat specifies in which format the resulting text should be returned, wehn * <code>getText()</code> is called. The following values are legal:<br> * <ul> * <li>PLAIN_STRING * <li>DISPLAY_FORMAT * <li>SPECIAL_SEPARATOR * <li>ENCRYPTED * </ul> * @param toolkit needed to gain access to <code>beep()</code> */ /*--------------------------------------------------------------------------*/ public RuleInputField(String format, String preset, String separator, String validator, Map<String, String> validatorParams, String processor, int resultFormat, Toolkit toolkit, InstallData idata) { this(format, preset, separator, validator, processor, resultFormat, toolkit, idata); this.validatorParams = validatorParams; this.hasParams = true; } /*--------------------------------------------------------------------------*/ /** * Constructs a rule input field. * * @param format a string that specifies the formatting and to a limited degree the behavior of * this field. * @param preset a string that specifies preset values for specific sub-fields. * @param separator a string to be used for separating the contents of individual fields in the * string returned by <code>getText()</code> * @param validator A string that specifies a class to perform validation services. The string * must completely identify the class, so that it can be instantiated. The class must implement * the <code>RuleValidator</code> interface. If an attempt to instantiate this class fails, no * validation will be performed. * @param processor A string that specifies a class to perform processing services. The string * must completely identify the class, so that it can be instantiated. The class must implement * the <code>Processor</code> interface. If an attempt to instantiate this class fails, no * processing will be performed. Instead, the text is returned in the default formatting. * @param resultFormat specifies in which format the resulting text should be returned, wehn * <code>getText()</code> is called. The following values are legal:<br> * <ul> * <li>PLAIN_STRING * <li>DISPLAY_FORMAT * <li>SPECIAL_SEPARATOR * <li>ENCRYPTED * </ul> * @param toolkit needed to gain access to <code>beep()</code> */ /*--------------------------------------------------------------------------*/ public RuleInputField(String format, String preset, String separator, String validator, String processor, int resultFormat, Toolkit toolkit, InstallData idata) { this.toolkit = toolkit; this.separator = separator; this.resultFormat = resultFormat; this.idata = idata; com.izforge.izpack.gui.FlowLayout layout = new com.izforge.izpack.gui.FlowLayout(); layout.setAlignment(com.izforge.izpack.gui.FlowLayout.LEFT); setLayout(layout); // ---------------------------------------------------- // attempt to create an instance of the Validator // ---------------------------------------------------- try { if (validator != null) { validationService = (Validator) Class.forName(validator).newInstance(); } } catch (Throwable exception) { validationService = null; Debug.trace(exception); } // ---------------------------------------------------- // attempt to create an instance of the Processor // ---------------------------------------------------- try { if (processor != null) { encryptionService = (Processor) Class.forName(processor).newInstance(); } } catch (Throwable exception) { encryptionService = null; Debug.trace(exception); } // ---------------------------------------------------- // create the fields and field separators // ---------------------------------------------------- createItems(format); if ((preset != null) && (preset.length() > 0)) { setFields(preset); } // ---------------------------------------------------- // set the focus to the first field // ---------------------------------------------------- activeField = (RuleTextField) inputFields.elementAt(0); activeField.grabFocus(); } /*--------------------------------------------------------------------------*/ /** * Returns the number of sub-fields composing this <code>RuleInputField</code>. * * @return the number of sub-fields */ /*--------------------------------------------------------------------------*/ public int getNumFields() { return (inputFields.size()); } /*--------------------------------------------------------------------------*/ /** * Returns the contents of the field indicated by <code>index</code>. * * @param index the index of the sub-field from which the contents is requested. * @return the contents of the indicated sub-field. * @throws IndexOutOfBoundsException if the index is out of bounds. */ /*--------------------------------------------------------------------------*/ public String getFieldContents(int index) throws IndexOutOfBoundsException { if ((index < 0) || (index > (inputFields.size() - 1))) { throw (new IndexOutOfBoundsException()); } return (((JTextField) inputFields.elementAt(index)).getText()); } /*--------------------------------------------------------------------------*/ // javadoc inherited public Map<String, String> getValidatorParams() { return validatorParams; } /*---------------------------------------------------------------------------*/ /** * Returns the field contents, assembled acording to the encryption and separator rules. * * @return the field contents */ /*--------------------------------------------------------------------------*/ public String getText() { Object item; StringBuffer buffer = new StringBuffer(); int size = inputFields.size(); // ---------------------------------------------------- // have the encryption service class perfrom the task // of assembling an output string. If we have no instance // of this class available set the formatting // instruction to the default setting. // ---------------------------------------------------- if (resultFormat == ENCRYPTED) { if (encryptionService != null) { buffer.append(encryptionService.process(this)); } else { resultFormat = DEFAULT; } } // ---------------------------------------------------- // concatentate the field contents, with no separators // in between. // ---------------------------------------------------- else if (resultFormat == PLAIN_STRING) { for (int i = 0; i < inputFields.size(); i++) { buffer.append(((JTextField) inputFields.elementAt(i)).getText()); } } // ---------------------------------------------------- // concatenate the field contents and setarators, as // specified for the display of the field. // ---------------------------------------------------- else if (resultFormat == DISPLAY_FORMAT) { for (int i = 0; i < items.size(); i++) { item = items.elementAt(i); if (item instanceof JTextField) { buffer.append(((JTextField) item).getText()); } else { buffer.append((String) item); } } } // ---------------------------------------------------- // concatenate the field contents and insert the // separator string in between. // ---------------------------------------------------- else if (resultFormat == SPECIAL_SEPARATOR) { for (int i = 0; i < size; i++) { buffer.append(((JTextField) inputFields.elementAt(i)).getText()); if (i < (size - 1)) { buffer.append(separator); } } } return (buffer.toString()); } /*--------------------------------------------------------------------------*/ /** * Creates the items that make up this field. Both separators and input fields are considered * items. The items created are stored in <code>items</code>. In addition, all fields are * stored in <code>inputFields</code>. * * @param format a string that specifies the layout of the input fields and separators. */ /*--------------------------------------------------------------------------*/ /* * $ @design * * I used a simple StringTokenizer to break the format string into individual tokens. The * approach in building up the field is to consider each token a potential definition for an * input field. Therefore I attempt to create an instance of FieldSpec from each token. * FieldSpec analyzes the token and if it does not represent a valid specification for an input * field throws an exception. If I catch an exception, I know the token does not represent a * valid field specification. In this case I treat the token as a separator, even though this * might not be what the user had intended. However, this approach allows me to implement robust * behavior (no exception thrown) even though the user might have made a mistake in the * definition. The mistake should become immediately obvious when testing the code, since a * input field definition would show up as separator between two fields. * -------------------------------------------------------------------------- */ private void createItems(String format) { Object item; JTextField field; String token; FieldSpec spec; StringTokenizer tokenizer = new StringTokenizer(format); while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); try { spec = new FieldSpec(token); field = new RuleTextField(spec.getColumns(), spec.getEditLength(), spec.getType(), spec.getUnlimitedEdit(), toolkit); // ------------------------------------------------ // if the previous item is also a field, insert a // space as separator // ------------------------------------------------ if (items.size() > 0) { item = items.lastElement(); if (item instanceof JTextField) { items.add(" "); } } items.add(field); inputFields.add(field); field.addFocusListener(this); field.addKeyListener(this); field.addCaretListener(this); } // -------------------------------------------------- // if we were not successful creating an input field, // the token must be a separator or the definition // has an error. Simply insert it as separator, // when testing the installer the error should become // obvious to the developer. // -------------------------------------------------- catch (Throwable exception) { if (items.size() == 0) { items.add(token); } else { item = items.lastElement(); // ---------------------------------------------- // if the previous item is also a separator, // simply concatenate the token with a space // inserted in between, don't add it as new // separator. // ---------------------------------------------- if (item instanceof String) { items.setElementAt(item + " " + token, (items.size() - 1)); } else { items.add(token); } } } } // ---------------------------------------------------- // add the fields and separators to the component // ---------------------------------------------------- for (int i = 0; i < items.size(); i++) { item = items.elementAt(i); if (item instanceof String) { add(new JLabel((String) item)); } else { add((JTextField) item); } } } /*--------------------------------------------------------------------------*/ /** * Sets each field to a pre-defined value. * * @param data a <code>String</code> containing the preset values for each field. The format * of the string is as follows: The content for the individuals fields must be separated by * whitespace. Each data block is preceeded by the index of the field to set (counting starts at * 0) followed by a colon ':'and after that the actual data for the field. */ /*--------------------------------------------------------------------------*/ private void setFields(String data) { StringTokenizer tokenizer = new StringTokenizer(data); String token; String indexString; int index; boolean process = false; String[] vals = null; int i = 0; vals = new String[tokenizer.countTokens()]; while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); indexString = token.substring(0, token.indexOf(':')); try { index = Integer.parseInt(indexString); if (index < inputFields.size()) { String val = token.substring((token.indexOf(':') + 1), token.length()); String className = ""; if (val.indexOf(":") > -1) { className = val.substring(val.indexOf(":") + 1); val = val.substring(0, val.indexOf(":")); } if (!"".equals(className) && !process) { process = true; } VariableSubstitutor vs = new VariableSubstitutor(idata.getVariables()); val = vs.substitute(val, null); vals[i] = val; i++; ((JTextField) inputFields.elementAt(index)).setText(val); } } catch (Throwable exception) { exception.printStackTrace(); } } if (process) { tokenizer = new StringTokenizer(data); while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); indexString = token.substring(0, token.indexOf(':')); try { index = Integer.parseInt(indexString); if (index < inputFields.size()) { String val = token.substring((token.indexOf(':') + 1), token.length()); String className = ""; String presult = ""; if (val.indexOf(":") > -1) { className = val.substring(val.indexOf(":") + 1); val = val.substring(0, val.indexOf(":")); } if (!"".equals(className)) { Processor p = (Processor) Class.forName(className).newInstance(); presult = p.process(this); } String[] td = new RE("\\*").split(presult); ((JTextField) inputFields.elementAt(index)).setText(td[index]); } } catch (Throwable exception) { } } } } /*--------------------------------------------------------------------------*/ /** * This method validates the field content. Validating is performed through a user supplied * service class that provides the validation rules. * * @return <code>true</code> if the validation passes or no implementation of a validation * rule exists. Otherwise <code>false</code> is returned. */ /*--------------------------------------------------------------------------*/ public boolean validateContents() { if (validationService != null) { return (validationService.validate(this)); } else { return (true); } } /*---------------------------------------------------------------------------* Implementation for KeyListener *---------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/ /** * This method is invoked when a key has been typed. The event occurs when a key press is * followed by a key release. * * @param event the key event forwarded by the system. */ /*--------------------------------------------------------------------------*/ public void keyTyped(KeyEvent event) { } /*--------------------------------------------------------------------------*/ /** * This method is invoked when a key has been pressed. This method verifies the condition of the * input field in focus. Once the column count in the field has reached the specified maximum, * the rule specified for the field in question is invoked. In case the test result is positive, * focus is set to the next field. If hte test result is negative, the field content is marked * and the caret set to the start of the field. * * @param event the key event forwarded by the system. */ /*--------------------------------------------------------------------------*/ public void keyPressed(KeyEvent event) { if (event.getKeyCode() == KeyEvent.VK_BACK_SPACE) { int caretPosition = activeField.getCaretPosition(); if (caretPosition == 0) { int activeIndex = inputFields.indexOf(activeField); if (activeIndex > 0) { activeIndex--; backstep = true; activeField = (RuleTextField) inputFields.elementAt(activeIndex); activeField.grabFocus(); } } } } /*--------------------------------------------------------------------------*/ /** * This method is invoked when a key has been released. * * @param event the key event forwarded by the system. */ /*--------------------------------------------------------------------------*/ public void keyReleased(KeyEvent event) { } /*---------------------------------------------------------------------------* Implementation for FocusListener *---------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/ /** * Invoked when a component gains the keyboard focus. * * @param event the focus event forwardes by the sytem. */ /*--------------------------------------------------------------------------*/ /* * $ @design <- keep this tag in place and don't write on this line! * * Enter design related documentation here. * -------------------------------------------------------------------------- */ public void focusGained(FocusEvent event) { activeField = (RuleTextField) event.getSource(); if (backstep) { activeField.setCaretPosition(activeField.getText().length()); backstep = false; } else { activeField.selectAll(); } } /*--------------------------------------------------------------------------*/ /** * Invoked when a component loses the keyboard focus. This method does nothing, we are only * interested in 'focus gained' events. * * @param event the focus event forwardes by the sytem. */ /*--------------------------------------------------------------------------*/ public void focusLost(FocusEvent event) { } /*---------------------------------------------------------------------------* Implementation for CaretListener *---------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/ /** * Called when the caret position is updated. * * @param event the caret event received from the text field */ /*--------------------------------------------------------------------------*/ public void caretUpdate(CaretEvent event) { if (activeField != null) { String text = activeField.getText(); int fieldSize = activeField.getEditLength(); int caretPosition = activeField.getCaretPosition(); int selection = activeField.getSelectionEnd() - activeField.getSelectionStart(); if ((!inputFields.lastElement().equals(activeField)) && (!activeField.unlimitedEdit())) { if ((text.length() == fieldSize) && (selection == 0) && (caretPosition == fieldSize) && !backstep) { activeField.transferFocus(); } } } } // ---------------------------------------------------------------------------- // // ---------------------------------------------------------------------------- private static class FieldSpec { private int MIN_TOKENS = 2; private int MAX_TOKENS = 3; private int type; private int columns; private int editLength; private boolean unlimitedEdit = false; public FieldSpec(String spec) throws Exception { StringTokenizer tokenizer = new StringTokenizer(spec, ":"); if ((tokenizer.countTokens() >= MIN_TOKENS) && (tokenizer.countTokens() <= MAX_TOKENS)) { String token = tokenizer.nextToken().toUpperCase(); // ------------------------------------------------ // test the first token for a valid type identifier // if it's valid assign the token to the type. // ------------------------------------------------ if ("N".equals(token)) { type = RuleTextField.N; } else if ("H".equals(token)) { type = RuleTextField.H; } else if ("A".equals(token)) { type = RuleTextField.A; } else if ("O".equals(token)) { type = RuleTextField.O; } else if ("AN".equals(token)) { type = RuleTextField.AN; } else { throw (new Exception()); } // ------------------------------------------------ // test for a valid integer to define the size // of the field and assing to columns. // ------------------------------------------------ try { token = tokenizer.nextToken(); columns = Integer.parseInt(token); } catch (Throwable exception) { throw (new Exception()); } // ------------------------------------------------ // test for a valid integer to define the edit // length and assign to editLength. If this fails // test for identifier for unlimited edit length. // If this works, set unlimitedEdit to true. // ------------------------------------------------ try { token = tokenizer.nextToken().toUpperCase(); editLength = Integer.parseInt(token); } catch (Throwable exception) { if ("U".equals(token)) { unlimitedEdit = true; } else { throw (new Exception()); } } } else { throw (new Exception()); } } public int getColumns() { return (columns); } public int getEditLength() { return (editLength); } public int getType() { return (type); } public boolean getUnlimitedEdit() { return (unlimitedEdit); } } // ---------------------------------------------------------------------------- } /*---------------------------------------------------------------------------*/