0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041: package com.sun.rave.web.ui.component;
0042:
0043: import java.lang.reflect.Array;
0044: import java.io.Serializable;
0045: import java.text.Collator;
0046: import java.text.MessageFormat;
0047: import java.util.ArrayList;
0048: import java.util.Comparator;
0049: import java.util.Iterator;
0050: import java.util.Locale;
0051: import java.util.Properties;
0052: import java.util.TreeMap;
0053: import javax.faces.FacesException;
0054: import javax.faces.application.FacesMessage;
0055: import javax.faces.component.EditableValueHolder;
0056: import javax.faces.component.UIComponent;
0057: import javax.faces.component.ValueHolder;
0058: import javax.faces.context.FacesContext;
0059: import javax.faces.convert.ConverterException;
0060: import javax.faces.el.EvaluationException;
0061: import javax.faces.el.MethodBinding;
0062: import javax.faces.el.MethodNotFoundException;
0063: import javax.faces.el.ValueBinding;
0064: import javax.faces.event.AbortProcessingException;
0065: import javax.faces.event.ActionEvent;
0066: import javax.faces.event.ActionListener; //import javax.faces.validator.ValidatorException;
0067: import com.sun.rave.web.ui.component.Button;
0068: import com.sun.rave.web.ui.component.TextField;
0069: import com.sun.rave.web.ui.component.Label;
0070: import com.sun.rave.web.ui.component.StaticText;
0071: import com.sun.rave.web.ui.model.list.ListItem;
0072: import com.sun.rave.web.ui.theme.Theme;
0073: import com.sun.rave.web.ui.util.MessageUtil;
0074: import com.sun.rave.web.ui.util.ThemeUtilities;
0075: import com.sun.rave.web.ui.util.ConversionUtilities;
0076: import com.sun.rave.web.ui.validator.StringLengthValidator;
0077:
0078: /**
0079: * <h4>About this tag.</h4>
0080: *
0081: * <p>This tag renders an EditableList component. Use this component
0082: * when web application users need to create and modify a list of
0083: * strings. The application user can add new strings by typing them
0084: * into the textfield and clicking the "Add" button, and remove them
0085: * by selecting one or more items from the list and clicking the
0086: * "Remove" button.</p>
0087: *
0088: * <h4>Configuring the listbox tag</h4>
0089: *
0090: * <p> Use the <code>list</code> attribute to bind the component
0091: * to a model. The value must be an EL expression that corresponds to
0092: * a managed bean or a property of a managed bean, and it must
0093: * evaluate to an array of <code>java.lang.String</code>.
0094: * </p>
0095: *
0096: * <p>To set the label of the textfield, use the
0097: * <code>fieldLabel</code> attribute. To set the label of the
0098: * textfield, use the <code>listLabel</code> attribute. To validate
0099: * new items, use the <code>fieldValidator</code> attribute; to
0100: * validate the contents of the list once the user has finished
0101: * adding and removing items, specify a <code>labelValidator</code>.</p>
0102: *
0103: * <h4>Facets</h4>
0104: *
0105: * <ul>
0106: * <li><code>fieldLabel</code>: use this facet to specify a custom
0107: * component for the textfield label.</li>
0108: * <li><code>listLabel</code>: use this facet to specify a custom
0109: * component for the textfield label.</li>
0110: * <li><code>field</code>: use this facet to specify a custom
0111: * component for the textfield.</li>
0112: * <li><code>addButton</code>: use this facet to specify a custom
0113: * component for the add button.</li>
0114: * <li><code>removeButton</code>: use this facet to specify a custom
0115: * component for the remove button.</li>
0116: * <li><code>search</code>: use this facet to specify a custom
0117: * component for the search button. </li>
0118: * <li><code>readOnly</code>: use this facet to specify a custom
0119: * component for display a readonly version of the component.</li>
0120: * <li><code>header</code>: use this facet to specify a header,
0121: * rendered in a table row above the component.</li>
0122: * <li><code>footer</code>: use this facet to specify a header,
0123: * rendered in a table row below the component.</li>
0124: * </ul>
0125: *
0126: * <h4>Client-side JavaScript functions</h4>
0127: *
0128: * <ul>
0129: * <li>NONE yet</li>
0130: * </ul>
0131: * @author avk
0132: */
0133: public class EditableList extends EditableListBase implements
0134: ListManager {
0135:
0136: /**
0137: * The component id for the ADD button
0138: */
0139: public static final String ADD_BUTTON_ID = "_addButton"; //NOI18N
0140: public static final String ADD_BUTTON_FACET = "addButton"; //NOI18N
0141: /**
0142: * The component ID for the remove button
0143: */
0144: public static final String REMOVE_BUTTON_ID = "_removeButton"; //NOI18N
0145: public static final String REMOVE_BUTTON_FACET = "removeButton"; //NOI18N
0146: /**
0147: * The component ID for the textfield
0148: */
0149: public static final String FIELD_ID = "_field"; //NOI18N
0150: public static final String FIELD_FACET = "field"; //NOI18N
0151:
0152: /**
0153: * The component ID for the textfield
0154: */
0155: public static final String LIST_LABEL_ID = "_listLabel"; //NOI18N
0156: public static final String LIST_LABEL_FACET = "listLabel"; //NOI18N
0157:
0158: /**
0159: * The component ID for the textfield
0160: */
0161: public static final String FIELD_LABEL_ID = "_fieldLabel"; //NOI18N
0162: public static final String FIELD_LABEL_FACET = "fieldLabel"; //NOI18N
0163:
0164: /**
0165: * The component ID for the textfield
0166: */
0167: public static final String READ_ONLY_ID = "_readOnly"; //NOI18N
0168: public static final String READ_ONLY_FACET = "readOnly"; //NOI18N
0169:
0170: /**
0171: * Facet name for the header facet
0172: */
0173: public static final String HEADER_FACET = "header"; //NOI18N
0174:
0175: /**
0176: * Facet name for the footer facet
0177: */
0178: public static final String FOOTER_FACET = "footer"; //NOI18N
0179:
0180: /**
0181: * The name of the component attribute that stores the name of the JavaScript object that organizes the JavsSCript functions for the component
0182: */
0183: public static final String JSOBJECT = "com.sun.rave.web.ui.EditabelListJS";
0184:
0185: /**
0186: * Name of the JavaScript function which is responsible for adding elements from the availble list to the selected list
0187: */
0188: public static final String ADD_FUNCTION = ".add(); ";
0189:
0190: /**
0191: * Name of the JavaScript function which is responsible for
0192: * enabling/disabling the add button
0193: */
0194: public static final String ENABLE_ADD_FUNCTION = ".enableAdd(); ";
0195:
0196: /**
0197: * Name of the JavaScript function which is responsible for
0198: * enabling/disabling the add button
0199: */
0200: public static final String SET_ADD_DISABLED_FUNCTION = ".setAddDisabled(false);";
0201:
0202: /**
0203: * Name of the JavaScript function which is responsible for
0204: * enabling/disabling the remove button
0205: */
0206: public static final String ENABLE_REMOVE_FUNCTION = ".enableRemove(); ";
0207:
0208: /**
0209: * Name of the JavaScript function that updates the buttons
0210: */
0211: public static final String UPDATE_BUTTONS_FUNCTION = ".updateButtons(); ";
0212: /**
0213: *
0214: * /**
0215: * Read only separator string
0216: */
0217: private static final String READ_ONLY_SEPARATOR = ", "; //NOI18N
0218:
0219: /**
0220: * Facet name for the search facet
0221: */
0222: public static final String SEARCH_FACET = "search"; //NOI18N
0223: public static final String SPACER_STRING = "_"; //NOI18N
0224:
0225: private static final int MIN_LENGTH = 20;
0226:
0227: private static final boolean DEBUG = false;
0228:
0229: private TreeMap listItems = null;
0230: private Collator collator = null;
0231: private transient Theme theme = null;
0232: private String selectedValue = null;
0233: private String[] valuesToRemove = null;
0234:
0235: /**
0236: * Get the maximum length of the strings on the list
0237: * @return An integer value for the maximum number of characters on the list
0238: */
0239: public int getMaxlLength() {
0240: int length = super .getMaxLength();
0241: if (length < 1) {
0242: length = 25;
0243: super .setMaxLength(length);
0244: }
0245: return length;
0246: }
0247:
0248: // Buttons
0249: /**
0250: * Get or create the ADD button. Retrieves the component specified by the
0251: * addButton facet (if there is one) or creates a new Button component.
0252: * @return A UI Component for the Add button
0253: */
0254: public UIComponent getAddButtonComponent() {
0255:
0256: if (DEBUG)
0257: log("getAddButtonComponent()");
0258:
0259: String id = getId();
0260:
0261: // Check if the page author has defined an addbutton facet
0262: UIComponent buttonComponent = getFacet(ADD_BUTTON_FACET);
0263: // If the page author has not defined a button facet,
0264: // check if the page author specified a button.
0265: if (buttonComponent == null) {
0266:
0267: buttonComponent = createButton(getTheme().getMessage(
0268: "EditableList.add"), //NOI18N
0269: ADD_BUTTON_ID, ADD_BUTTON_FACET, new AddListener());
0270: ((Button) buttonComponent).setPrimary(true);
0271: } else if (DEBUG) {
0272: log("\tFound facet."); //NOI18N
0273: }
0274: if (buttonComponent != null && isDisabled()) {
0275: buttonComponent.getAttributes().put("disabled",
0276: Boolean.TRUE); //NOI18N
0277: }
0278: return buttonComponent;
0279: }
0280:
0281: /**
0282: * Get or create the REMOVE button. Retrieves the component specified by the
0283: * removeButton facet (if there is one) or creates a new Button component.
0284: * @return A UI Component for the REMOVE button
0285: */
0286: public UIComponent getRemoveButtonComponent() {
0287:
0288: if (DEBUG)
0289: log("getRemoveButtonComponent()");
0290:
0291: String id = getId();
0292:
0293: // Check if the page author has defined an addbutton facet
0294: UIComponent buttonComponent = getFacet(REMOVE_BUTTON_FACET);
0295:
0296: // If the page author has not defined a button facet,
0297: // check if the page author specified a button.
0298: if (buttonComponent == null) {
0299:
0300: buttonComponent = createButton(
0301: getTheme().getMessage("EditableList.remove"), //NOI18N
0302: REMOVE_BUTTON_ID, REMOVE_BUTTON_FACET,
0303: new RemoveListener());
0304: } else if (DEBUG) {
0305: log("\tFound facet."); //NOI18N
0306: }
0307:
0308: if (buttonComponent != null && isDisabled()) {
0309: buttonComponent.getAttributes().put("disabled",
0310: Boolean.TRUE); //NOI18N
0311: }
0312:
0313: return buttonComponent;
0314:
0315: }
0316:
0317: // Labels
0318: /**
0319: * Gets or creates a component for the list label. Retrieves the
0320: * listLabel facet if one was specified, or creates a new label component.
0321: * @return A UIComponent for the list label
0322: */
0323: public UIComponent getListLabelComponent() {
0324:
0325: if (DEBUG)
0326: log("getListLabelComponent()");
0327:
0328: String id = getId();
0329:
0330: // Check if the page author has defined a label facet
0331: UIComponent labelComponent = getFacet(LIST_LABEL_FACET);
0332:
0333: // If the page author has not defined a label facet,
0334: // check if the page author specified a label.
0335: if (labelComponent == null) {
0336:
0337: String labelString = getListLabel();
0338: if (labelString == null || labelString.length() == 0) {
0339: labelString = getTheme().getMessage(
0340: "EditableList.defaultListLabel"); //NOI18N
0341: }
0342:
0343: labelComponent = createLabel(labelString, LIST_LABEL_ID,
0344: LIST_LABEL_FACET);
0345:
0346: ((Label) labelComponent).setLabeledComponent(this );
0347: } else if (DEBUG) {
0348: log("\tFound facet."); //NOI18N
0349: }
0350:
0351: return labelComponent;
0352: }
0353:
0354: /**
0355: * Gets or creates a component for the textfield label. Retrieves the
0356: * fieldLabel facet if one was specified, or creates a new label component.
0357: * @return A UIComponent for the field label
0358: */
0359: public UIComponent getFieldLabelComponent() {
0360:
0361: if (DEBUG)
0362: log("getFieldLabelComponent()");
0363:
0364: String id = getId();
0365:
0366: // Check if the page author has defined a label facet
0367: UIComponent labelComponent = getFacet(FIELD_LABEL_FACET);
0368: // If the page author has not defined a label facet,
0369: // check if the page author specified a label.
0370: if (labelComponent == null) {
0371:
0372: String labelString = getFieldLabel();
0373: if (labelString == null || labelString.length() == 0) {
0374: labelString = getTheme().getMessage(
0375: "EditableList.defaultFieldLabel"); //NOI18N
0376: }
0377:
0378: labelComponent = createLabel(labelString, FIELD_LABEL_ID,
0379: FIELD_LABEL_FACET);
0380:
0381: ((Label) labelComponent)
0382: .setLabeledComponent(getFieldComponent());
0383: } else if (DEBUG) {
0384: log("\tFound facet.");//NOI18N
0385: }
0386:
0387: return labelComponent;
0388: }
0389:
0390: // Other
0391: /**
0392: * Gets or creates a component for the textfield. Retrieves the
0393: * field facet if one was specified, or creates a new TextField component.
0394: * @return A UIComponent for the textfield
0395: */
0396: public UIComponent getFieldComponent() {
0397:
0398: if (DEBUG)
0399: log("getFieldComponent()");
0400:
0401: String id = getId();
0402:
0403: // Check if the page author has defined a label facet
0404: UIComponent fieldComponent = getFacet(FIELD_FACET);
0405:
0406: // If the page author has not defined a field facet,
0407: // check if the page author specified a field.
0408: if (fieldComponent == null) {
0409:
0410: fieldComponent = createField(FIELD_ID, FIELD_FACET);
0411: } else if (DEBUG) {
0412: log("\tFound facet.");//NOI18N
0413: }
0414:
0415: if (fieldComponent != null && isDisabled()) {
0416: fieldComponent.getAttributes()
0417: .put("disabled", Boolean.TRUE);//NOI18N
0418: }
0419: return fieldComponent;
0420:
0421: }
0422:
0423: // Readonly value
0424: /**
0425: * Return a string suitable for displaying the value in read only mode.
0426: * The default is to separate the list values with a comma.
0427: *
0428: * @param context The FacesContext
0429: * @throws javax.faces.FacesException If the list items cannot be processed
0430: */
0431: protected String getValueAsReadOnly(FacesContext context)
0432: throws FacesException {
0433:
0434: // The comma format READ_ONLY_SEPARATOR should be part of the theme
0435: // and/or configurable by the application
0436: //
0437: StringBuffer valueBuffer = new StringBuffer(200);
0438:
0439: Iterator iterator = getListItems(context, false);
0440:
0441: while (iterator.hasNext()) {
0442: String string = ((ListItem) (iterator.next())).getLabel();
0443: // Do this with a boolean on getListItems instead
0444: if (string.indexOf("nbsp") > -1) { //NOI18N
0445: continue;
0446: }
0447: valueBuffer.append(string);
0448: if (iterator.hasNext()) {
0449: valueBuffer.append(READ_ONLY_SEPARATOR);
0450: }
0451: }
0452: return valueBuffer.toString();
0453: }
0454:
0455: /**
0456: * Creates a component for the EditableList in case the component
0457: * is read-only.
0458: * @return A UIComponent that displays the read-only value
0459: */
0460: public UIComponent getReadOnlyValueComponent() {
0461:
0462: if (DEBUG)
0463: log("getReadOnlyValueComponent()");
0464:
0465: String id = getId();
0466:
0467: // Check if the page author has defined a label facet
0468: UIComponent textComponent = getFacet(READ_ONLY_FACET); //NOI18N
0469:
0470: // If the page author has not defined a label facet,
0471: // check if the page author specified a label.
0472: if (textComponent == null) {
0473: FacesContext context = FacesContext.getCurrentInstance();
0474: textComponent = createText(getValueAsReadOnly(context), //NOI18N
0475: READ_ONLY_ID, READ_ONLY_FACET);
0476: } else if (DEBUG) {
0477: log("\tFound facet."); //NOI18N
0478: }
0479: return textComponent;
0480: }
0481:
0482: // The following methods overrides default behaviour that does not
0483: // make sense for this component
0484: /**
0485: *
0486: * @param converter
0487: */
0488: public void setConverter(javax.faces.convert.Converter converter) {
0489: String msg = getTheme().getMessage("EditableList.noConversion"); //NOI18N
0490: throw new RuntimeException(msg);
0491: }
0492:
0493: public String getJavaScriptObjectName() {
0494: Object o = this .getAttributes().get(JSOBJECT);
0495: String name = null;
0496: if (o != null && o instanceof String) {
0497: name = (String) o;
0498: } else {
0499: FacesContext context = FacesContext.getCurrentInstance();
0500: name = this .getClientId(context).replace(':', '_');
0501: name = "EditableList_".concat(name);
0502: this .getAttributes().put(JSOBJECT, name);
0503: }
0504: return name;
0505: }
0506:
0507: private Theme getTheme() {
0508: return ThemeUtilities.getTheme(FacesContext
0509: .getCurrentInstance());
0510: }
0511:
0512: public String getOnChange() {
0513: StringBuffer onchangeBuffer = new StringBuffer(128);
0514: onchangeBuffer.append(getJavaScriptObjectName());
0515: onchangeBuffer.append(ENABLE_REMOVE_FUNCTION);
0516: return onchangeBuffer.toString();
0517: }
0518:
0519: public String getPrimaryElementID(FacesContext context) {
0520:
0521: // This is a little sketchy, because in this case we'd actually prefer
0522: // to return different values for focus and for the labelled component.
0523: // We should always label the list (that's the one that should have the
0524: // invalid icon if the list is empty, for example. But we should
0525: // probably also set the focus to the top input component which could
0526: // be either the field or the label. Ah well. I can get around this
0527: // if I implement some extra bits on the label.
0528: // TODO
0529: return getClientId(context).concat(ListSelector.LIST_ID);
0530: }
0531:
0532: /**
0533: * Getter for property valuesToRemove.
0534: * @return Value of property valuesToRemove.
0535: */
0536: public String[] getValuesToRemove() {
0537: if (valuesToRemove == null) {
0538: return new String[0];
0539: }
0540: return this .valuesToRemove;
0541: }
0542:
0543: /**
0544: * Setter for property valuesToRemove.
0545: * @param valuesToRemove New value of property valuesToRemove.
0546: */
0547: public void setValuesToRemove(String[] valuesToRemove) {
0548:
0549: this .valuesToRemove = valuesToRemove;
0550: }
0551:
0552: private Label createLabel(String labelString, String id,
0553: String facetName) {
0554:
0555: if (DEBUG)
0556: log("createLabel()");
0557:
0558: // If we find a label, define a component and add it to the
0559: // children, unless it has been added in a previous cycle
0560: // (the component is being redisplayed).
0561:
0562: if (labelString == null || labelString.length() < 1) {
0563: // TODO - maybe print a default?
0564: labelString = new String();
0565: }
0566:
0567: Label label = new Label();
0568: label.setId(getId().concat(id));
0569: label.setText(labelString);
0570: label.setLabelLevel(getLabelLevel());
0571: this .getFacets().put(facetName, label);
0572: return label;
0573: }
0574:
0575: private StaticText createText(String text, String id,
0576: String facetName) {
0577:
0578: if (DEBUG)
0579: log("createText()");
0580:
0581: // If we find a label, define a component and add it to the
0582: // children, unless it has been added in a previous cycle
0583: // (the component is being redisplayed).
0584:
0585: if (text == null || text.length() < 1) {
0586: // TODO - maybe print a default?
0587: text = new String();
0588: }
0589:
0590: StaticText field = new StaticText();
0591: field.setValue(text);
0592: field.setId(getId().concat(id));
0593: getFacets().put(facetName, field);
0594: return field;
0595: }
0596:
0597: private TextField createField(String id, String facetName) {
0598:
0599: if (DEBUG)
0600: log("createField()");
0601:
0602: String jsObjectName = getJavaScriptObjectName();
0603: StringBuffer onkeypressBuffer = new StringBuffer(128);
0604: onkeypressBuffer.append("if(event.keyCode == 13) { "); //NOI18N
0605: onkeypressBuffer.append(jsObjectName);
0606: onkeypressBuffer.append(ADD_FUNCTION);
0607: onkeypressBuffer.append("return false; } "); //NOI18N
0608:
0609: StringBuffer onfocusBuffer = new StringBuffer(128);
0610: onfocusBuffer.append(jsObjectName);
0611: onfocusBuffer.append(SET_ADD_DISABLED_FUNCTION);
0612: onfocusBuffer.append("return false;"); //NOI18N
0613:
0614: StringBuffer onfocuslostBuffer = new StringBuffer(128);
0615: onfocuslostBuffer.append(jsObjectName);
0616: onfocuslostBuffer.append(ENABLE_ADD_FUNCTION);
0617: onfocuslostBuffer.append("return false;"); //NOI18N
0618:
0619: TextField field = new TextField();
0620: field.setId(getId().concat(id));
0621: int columns = getMaxLength();
0622: if (columns < MIN_LENGTH) {
0623: columns = MIN_LENGTH;
0624: }
0625:
0626: field.setColumns(columns);
0627: field.setOnKeyPress(onkeypressBuffer.toString());
0628: field.setOnFocus(onfocusBuffer.toString());
0629: field.setOnBlur(onfocuslostBuffer.toString());
0630: field.setTrim(true);
0631: if (getFieldValidator() != null) {
0632: field.setValidator(getFieldValidator());
0633: }
0634: if (getTabIndex() > 0) {
0635: field.setTabIndex(getTabIndex());
0636: }
0637:
0638: Theme theme = getTheme();
0639: StringLengthValidator strl = new StringLengthValidator(
0640: getMaxLength(), 1);
0641: strl.setTooLongMessage(theme
0642: .getMessage("EditableList.itemTooLong"));
0643: strl.setTooShortMessage(theme
0644: .getMessage("EditableList.fieldEmpty"));
0645: field.addValidator(strl);
0646: getFacets().put(facetName, field);
0647: return field;
0648: }
0649:
0650: private Button createButton(String text, String id,
0651: String facetName, ActionListener actionListener) {
0652:
0653: if (DEBUG)
0654: log("createButton()");
0655:
0656: Button button = new Button();
0657: button.setId(getId().concat(id));
0658: button.setText(text);
0659: if (getTabIndex() > 0) {
0660: button.setTabIndex(getTabIndex());
0661: }
0662: button.setImmediate(true);
0663: button.addActionListener(actionListener);
0664: getFacets().put(facetName, button);
0665: return button;
0666: }
0667:
0668: /**
0669: * Retrieve an Iterator of ListSelector.ListItem, to be used by the
0670: * renderer.
0671: * @return an Iterator over {@link ListItem}.
0672: * @throws javax.faces.FacesException
0673: */
0674: public Iterator getListItems(FacesContext context,
0675: boolean rulerAtEnd) throws FacesException {
0676:
0677: if (DEBUG)
0678: log("getListItems()");
0679:
0680: Locale locale = context.getViewRoot().getLocale();
0681: if (DEBUG)
0682: log("\tLocale is " + locale.toString());
0683: collator = Collator.getInstance(locale);
0684: listItems = new TreeMap(collator);
0685:
0686: // We have to make sure that the long empty list item (whose
0687: // purpose is to guarantee that the size of the list stays
0688: // constant) is alwasy at the bottom of the list. (=has the
0689: // highest key in the map). We do this by identifying the
0690: // longest key in the map, as long as the collator is
0691: // concerned and appending something at the end. (It's not
0692: // possible to use a constant for this, since an o with an
0693: // umlaut comes after z in Swedish, but before it in German,
0694: // for example).
0695: String lastKey = ""; //NOI18N
0696: String[] currentValues = getCurrentValueAsStringArray();
0697: if (DEBUG) {
0698: log("\tValues are:");
0699: for (int i = 0; i < currentValues.length; ++i) {
0700: log(currentValues[i]);
0701: }
0702: }
0703:
0704: // The string currently being evaluated
0705: String currentString;
0706:
0707: // Two cases:
0708: // First case: the page author requested a sorted map (by
0709: // character), in which case we sort by the strings
0710: // themselves. The last key is set to the string that the
0711: // collator deems to be the last.
0712: // Second case: the list is sorted by the order they were
0713: // added to the map. We deal with that by generating a
0714: // successively longer key for each entry (maps do not
0715: // conserve the order the items were added). The last key
0716: // is set to the last key generated.
0717:
0718: ListItem listItem = null;
0719: // If the page author does not want the list items to be
0720: // sorted (alphabetically by locale), then they're
0721: // supposed to be sorted by the order they were added.
0722: // Maps are not guaranteed to return items in the order
0723: // they were added, so we have to create this order
0724: // artificially. We do that by creating a successively
0725: // longer key for each element. (a, aa, aaa...).
0726: StringBuffer unsortedKeyBuffer = new StringBuffer("a"); //NOI18N
0727: for (int counter = 0; counter < currentValues.length; ++counter) {
0728: currentString = currentValues[counter];
0729: if (DEBUG) {
0730: log("Current string is " + currentString); //NOI18N
0731: log("SelectedValue is " + String.valueOf(selectedValue)); //NOI18N
0732: }
0733:
0734: if (currentString == null) {
0735: String msg = MessageUtil.getMessage(
0736: "com.sun.rave.web.ui.resources.LogMessages",
0737: "EditableList.badValue",
0738: new Object[] { getClientId(context) });
0739: throw new FacesException(msg);
0740: }
0741:
0742: listItem = new ListItem(currentString);
0743: listItem.setValue(currentString);
0744: if (currentString.equals(selectedValue)) {
0745: if (DEBUG)
0746: log("Selected value");
0747: listItem.setSelected(true);
0748: }
0749: if (isSorted()) {
0750: if (collator.compare(currentString, lastKey) > 0) {
0751: lastKey = currentString;
0752: }
0753: listItems.put(currentString, listItem);
0754: } else {
0755: listItems.put(unsortedKeyBuffer.toString(), listItem);
0756: unsortedKeyBuffer.append("a"); //NOI18N
0757: }
0758: }
0759: if (!isSorted()) {
0760: lastKey = unsortedKeyBuffer.toString();
0761: }
0762:
0763: // rulerAtEnd will be true if the invoker needs a blank
0764: // disabled list option at the end. Typically this is
0765: // needed by the renderer, to guarantee that the widget
0766: // stays the same in size when items are added and removed.
0767: if (rulerAtEnd) {
0768:
0769: int length = getMaxlLength();
0770: if (length < MIN_LENGTH) {
0771: length = MIN_LENGTH;
0772: }
0773: StringBuffer labelBuffer = new StringBuffer(length);
0774:
0775: for (int counter = 0; counter < length; ++counter) {
0776: labelBuffer.append(SPACER_STRING); //NOI18N
0777: }
0778: ListItem item = new ListItem(labelBuffer.toString());
0779: item.setDisabled(true);
0780: listItems.put(lastKey.concat("a"), item); //NOI18N
0781: }
0782:
0783: return listItems.values().iterator();
0784: }
0785:
0786: private String[] getCurrentValueAsStringArray() {
0787:
0788: if (DEBUG)
0789: log("getCurrentValueAsStringArray()");
0790: Object value = getSubmittedValue();
0791: if (value == null) {
0792: if (DEBUG)
0793: log("\tUsing regular value");
0794: value = getValue();
0795: } else if (DEBUG)
0796: log("\tUsing submitted value");
0797:
0798: if (value == null) {
0799: return new String[0];
0800: }
0801: if (value instanceof String[]) {
0802: return (String[]) value;
0803: }
0804:
0805: String msg = MessageUtil.getMessage(
0806: "com.sun.rave.web.ui.resources.LogMessages",
0807: "EditableList.badValue",
0808: new Object[] { getClientId(FacesContext
0809: .getCurrentInstance()) });
0810: throw new FacesException(msg);
0811: }
0812:
0813: private void log(String s) {
0814: System.out.println(this .getClass().getName() + "::" + s); //NOI18N
0815: }
0816:
0817: /**
0818: * Retrieve the value of this component (the "selected" property) as an
0819: * object. This method is invoked by the JSF engine during the validation
0820: * phase. The JSF default behaviour is for components to defer the
0821: * conversion and validation to the renderer, but for the Selector based
0822: * components, the renderers do not share as much functionality as the
0823: * components do, so it is more efficient to do it here.
0824: * @param context The FacesContext of the request
0825: * @param submittedValue The submitted value of the component
0826: */
0827:
0828: public Object getConvertedValue(FacesContext context,
0829: Object submittedValue) throws ConverterException {
0830:
0831: if (DEBUG)
0832: log("getConvertedValue()");
0833:
0834: if (!(submittedValue instanceof String[])) {
0835: throw new ConverterException(
0836: "Submitted value must be a String array");
0837: }
0838: String[] rawValues = (String[]) submittedValue;
0839:
0840: // If there are no elements in rawValues nothing was submitted.
0841: // If null was rendered, return null
0842: //
0843: if (rawValues.length == 0) {
0844: if (ConversionUtilities.renderedNull(this )) {
0845: return null;
0846: }
0847: }
0848: return submittedValue;
0849: }
0850:
0851: /**
0852: * @exception NullPointerException
0853: */
0854: public void processValidators(FacesContext context) {
0855:
0856: if (context == null) {
0857: throw new NullPointerException();
0858: }
0859:
0860: // Skip processing if our rendered flag is false
0861: if (!isRendered()) {
0862: return;
0863: }
0864:
0865: // This component may be a developer defined facet.
0866: // It is explicitly validated during an Add action.
0867: // It must not be validated during a submit. The assumption
0868: // is that processValidators is being called during
0869: // a submit and not in an immediate context.
0870: // Compare the id of this component with the children
0871: // and facets and if it matches don't call its
0872: // processValidators method.
0873: //
0874: UIComponent field = getFieldComponent();
0875: String fieldId = field.getId();
0876:
0877: // Process all the facets and children of this component
0878: Iterator kids = getFacetsAndChildren();
0879: while (kids.hasNext()) {
0880: UIComponent kid = (UIComponent) kids.next();
0881: // We probably should ensure that fieldId is not
0882: // null, during getFieldComponent() even if
0883: // it is a developer defined facet.
0884: //
0885: if (fieldId != null && fieldId.equals(kid.getId())) {
0886: continue;
0887: }
0888: kid.processValidators(context);
0889: }
0890:
0891: // Validate the EditableList
0892: //
0893: checkValid(context);
0894: }
0895:
0896: public void processAddAction() {
0897:
0898: if (DEBUG)
0899: log("processAddAction()");
0900:
0901: // If we are rendering prematurely don't do anything
0902: //
0903: if (FacesContext.getCurrentInstance().getRenderResponse()) {
0904: return;
0905: }
0906:
0907: selectedValue = null;
0908:
0909: String[] values = getCurrentValueAsStringArray();
0910:
0911: Object value = getAddedObject();
0912: if (value == null) {
0913: return;
0914: }
0915: //TODO - fix this when implementing conversion for this component
0916: selectedValue = value.toString();
0917:
0918: int numValues = values.length;
0919:
0920: String[] newValues = new String[numValues + 1];
0921: int counter;
0922: for (counter = 0; counter < numValues; ++counter) {
0923: newValues[counter] = values[counter];
0924: if (DEBUG)
0925: log("\tAdding " + newValues[counter]);
0926: }
0927: newValues[counter] = selectedValue;
0928: if (DEBUG)
0929: log("\tAdding " + newValues[counter]);
0930: setSubmittedValue(newValues);
0931: }
0932:
0933: public void processRemoveAction() {
0934:
0935: if (DEBUG)
0936: log("processRemoveAction()");
0937:
0938: // If we are rendering prematurely don't do anything
0939: //
0940: if (FacesContext.getCurrentInstance().getRenderResponse()) {
0941: return;
0942: }
0943:
0944: // Reset the selected value
0945: selectedValue = null;
0946:
0947: ArrayList items = new ArrayList();
0948: int counter;
0949:
0950: if (getValue() != null) {
0951: if (DEBUG)
0952: log("\tList was not empty");
0953: String[] strings = getCurrentValueAsStringArray();
0954: int length = strings.length;
0955: for (counter = 0; counter < length; ++counter) {
0956: items.add(strings[counter]);
0957: if (DEBUG)
0958: log("Added " + strings[counter]);
0959: }
0960: }
0961:
0962: String[] valuesToRemove = getValuesToRemove();
0963: for (counter = 0; counter < valuesToRemove.length; ++counter) {
0964: items.remove(valuesToRemove[counter]);
0965: if (DEBUG)
0966: log("remove " + valuesToRemove[counter]);
0967: }
0968:
0969: String[] newValues = new String[items.size()];
0970: for (counter = 0; counter < items.size(); ++counter) {
0971: newValues[counter] = (String) (items.get(counter));
0972: if (DEBUG)
0973: log("\tAdding back " + newValues[counter]);
0974: }
0975:
0976: setValuesToRemove(null);
0977: setSubmittedValue(newValues);
0978: }
0979:
0980: private void checkValid(FacesContext context) {
0981:
0982: if (DEBUG)
0983: log("checkValid()");
0984:
0985: try {
0986: validate(context);
0987: } catch (RuntimeException e) {
0988: if (DEBUG)
0989: log("Error during validation");
0990: context.renderResponse();
0991: throw e;
0992: }
0993:
0994: if (!isValid()) {
0995: if (DEBUG)
0996: log("Component is not valid");
0997: context.renderResponse();
0998: }
0999: }
1000:
1001: private Object getAddedObject() {
1002:
1003: FacesContext context = FacesContext.getCurrentInstance();
1004:
1005: if (DEBUG)
1006: log("\tAdd a new item");
1007:
1008: // Need to get the field's value validated first
1009: // The field can't be immediate because we don't want
1010: // to validate it if the value is not going to be added.
1011: // For example in a real submit request.
1012: //
1013: EditableValueHolder field = (EditableValueHolder) getFieldComponent();
1014:
1015: // This is ok to do here.
1016: // We are currently after the APPLY_REQUEST_VALUES phase
1017: // and before the PROCESS_VALIDATIONS phase.
1018: // If the field were marked immediate, then the validation
1019: // would have occured before we get here. If not done
1020: // here it will be done in the next phase. But it needs
1021: // to be done here, sort a simulation of immediate
1022: // henavior. But we don't want the side effect of immediate
1023: // behavior from external immediate components.
1024: //
1025: ((UIComponent) field).processValidators(context);
1026:
1027: if (!field.isValid()) {
1028: return null;
1029: }
1030: // Get the value from the field.
1031: //
1032: Object value = field.getValue();
1033:
1034: // This is a policy of the EditableList.
1035: // An emptyString or null value cannot be added to the list.
1036: //
1037: if (value == null
1038: || (value instanceof String && value.toString()
1039: .length() == 0)) {
1040:
1041: field.setValid(false);
1042: context.renderResponse();
1043:
1044: if (DEBUG)
1045: log("No value from the field");
1046:
1047: String message = ThemeUtilities.getTheme(context)
1048: .getMessage("EditableList.fieldEmpty");
1049: context.addMessage(getClientId(context), new FacesMessage(
1050: message));
1051: return null;
1052: }
1053: // The new value was added so clear the value.
1054: // This set is questionable, if the field is a developer
1055: // defined facet. This will cause an update to the model
1056: // before the value change event.
1057: //
1058: if (DEBUG)
1059: log("\tFound new value: " + value);
1060: field.setValue(null);
1061:
1062: return value;
1063: }
1064:
1065: /* Don't need this. Only needed for debugging.
1066: public void setValue(Object value) {
1067:
1068: if(DEBUG) log("setValue()...");
1069: super.setValue(value);
1070: if(DEBUG) log("\tLocal value set: " +
1071: String.valueOf(isLocalValueSet()));
1072: }
1073: */
1074:
1075: /** Always returns false for EditableList **/
1076: public boolean isImmediate() {
1077: return false;
1078: }
1079:
1080: /**
1081: * <p>Return <code>true</code> if the new value is different from the
1082: * previous value.</p>
1083: *
1084: * This only implements a compareValues for value if it is an Array.
1085: * If value is not an Array, defer to super.compareValues.
1086: * The assumption is that the ordering of the elements
1087: * between the previous value and the new value is determined
1088: * in the same manner.
1089: *
1090: * Another assumption is that the two object arguments
1091: * are of the same type, both arrays of both not arrays.
1092: *
1093: * @param previous old value of this component (if any)
1094: * @param value new value of this component (if any)
1095: */
1096: protected boolean compareValues(Object previous, Object value) {
1097:
1098: // Let super take care of null cases
1099: //
1100: if (previous == null || value == null) {
1101: return super .compareValues(previous, value);
1102: }
1103: if (value instanceof Object[]) {
1104: // If the lengths aren't equal return true
1105: //
1106: int length = Array.getLength(value);
1107: if (Array.getLength(previous) != length) {
1108: return true;
1109: }
1110: // Each element at index "i" in previous must be equal to the
1111: // elementa at index "i" in value.
1112: //
1113: for (int i = 0; i < length; ++i) {
1114:
1115: Object newValue = Array.get(value, i);
1116: Object prevValue = Array.get(previous, i);
1117:
1118: // This is probably not necessary since
1119: // an Option's value cannot be null
1120: //
1121: if (newValue == null) {
1122: if (prevValue == null) {
1123: continue;
1124: } else {
1125: return true;
1126: }
1127: }
1128: if (prevValue == null) {
1129: return true;
1130: }
1131:
1132: if (!prevValue.equals(newValue)) {
1133: return true;
1134: }
1135: }
1136: return false;
1137: }
1138: return super .compareValues(previous, value);
1139: }
1140:
1141: public String[] getValueAsStringArray(FacesContext context) {
1142:
1143: if (DEBUG)
1144: log("getValueAsStringArray)");
1145:
1146: Iterator iterator = getListItems(context, false);
1147: int numItems = listItems.size();
1148: String[] values = new String[numItems];
1149:
1150: int counter = 0;
1151: while (counter < numItems) {
1152: values[counter] = ((ListItem) (iterator.next())).getValue();
1153: ++counter;
1154: }
1155: return values;
1156: }
1157:
1158: public boolean mainListSubmits() {
1159: return true;
1160: }
1161: }
1162:
1163: class AddListener implements ActionListener, Serializable {
1164:
1165: public void processAction(ActionEvent event) {
1166:
1167: UIComponent comp = event.getComponent();
1168: comp = comp.getParent();
1169: if (comp instanceof EditableList) {
1170: ((EditableList) comp).processAddAction();
1171: }
1172: }
1173: }
1174:
1175: class RemoveListener implements ActionListener, Serializable {
1176:
1177: public void processAction(ActionEvent event) {
1178:
1179: UIComponent comp = event.getComponent();
1180: comp = comp.getParent();
1181: if (comp instanceof EditableList) {
1182: ((EditableList) comp).processRemoveAction();
1183: }
1184: }
1185:
1186: }
|