001: /**********************************************************************************
002: *
003: * $Id: FacesUtil.java 22226 2007-03-06 17:42:53Z ray@media.berkeley.edu $
004: *
005: ***********************************************************************************
006: *
007: * Copyright (c) 2005 The Regents of the University of California, The MIT Corporation
008: *
009: * Licensed under the Educational Community License, Version 1.0 (the "License");
010: * you may not use this file except in compliance with the License.
011: * You may obtain a copy of the License at
012: *
013: * http://www.opensource.org/licenses/ecl1.php
014: *
015: * Unless required by applicable law or agreed to in writing, software
016: * distributed under the License is distributed on an "AS IS" BASIS,
017: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018: * See the License for the specific language governing permissions and
019: * limitations under the License.
020: *
021: **********************************************************************************/package org.sakaiproject.tool.gradebook.jsf;
022:
023: import java.math.BigDecimal;
024: import java.text.MessageFormat;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Map;
029:
030: import javax.faces.application.FacesMessage;
031: import javax.faces.component.UIComponent;
032: import javax.faces.component.UIData;
033: import javax.faces.component.UIInput;
034: import javax.faces.component.UIParameter;
035: import javax.faces.context.FacesContext;
036: import javax.faces.event.FacesEvent;
037:
038: import org.apache.commons.logging.Log;
039: import org.apache.commons.logging.LogFactory;
040: import org.sakaiproject.jsf.util.LocaleUtil;
041: import org.sakaiproject.tool.gradebook.ui.MessagingBean;
042:
043: /**
044: * A noninstantiable utility class, because every JSF project needs one.
045: */
046: public class FacesUtil {
047: private static final Log logger = LogFactory
048: .getLog(FacesUtil.class);
049:
050: /**
051: * Before display, scores are rounded at this number of decimal
052: * places and later truncated to (hopefully) a shorter number.
053: */
054: public static int MAXIMUM_MEANINGFUL_DECIMAL_PLACES = 5;
055:
056: // Enforce noninstantiability.
057: private FacesUtil() {
058: }
059:
060: /**
061: * If the JSF h:commandLink component includes f:param children, those name-value pairs
062: * are put into the request parameter map for later use by the action handler. Unfortunately,
063: * the same isn't done for h:commandButton. This is a workaround to let arguments
064: * be associated with a button.
065: *
066: * Because action listeners are guaranteed to be executed before action methods, an
067: * action listener can use this method to update any context the action method might need.
068: */
069: public static final Map getEventParameterMap(FacesEvent event) {
070: Map parameterMap = new HashMap();
071: List children = event.getComponent().getChildren();
072: for (Iterator iter = children.iterator(); iter.hasNext();) {
073: Object next = iter.next();
074: if (next instanceof UIParameter) {
075: UIParameter param = (UIParameter) next;
076: parameterMap.put(param.getName(), param.getValue());
077: }
078: }
079: if (logger.isDebugEnabled())
080: logger.debug("parameterMap=" + parameterMap);
081: return parameterMap;
082: }
083:
084: /**
085: * To cut down on configuration noise, allow access to request-scoped beans from
086: * session-scoped beans, and so on, this method lets the caller try to find
087: * anything anywhere that Faces can look for it.
088: *
089: * WARNING: If what you're looking for is a managed bean and it isn't found,
090: * it will be created as a result of this call.
091: */
092: public static final Object resolveVariable(String name) {
093: FacesContext context = FacesContext.getCurrentInstance();
094: return context.getApplication().getVariableResolver()
095: .resolveVariable(context, name);
096: }
097:
098: /**
099: * Because POST arguments aren't carried over redirects, the easiest way to
100: * get bookmarkable URLs is to use "h:outputLink" rather than "h:commandLink" or
101: * "h:commandButton", and to add query string parameters via "f:param". However,
102: * if the value of the output link is something like "editAsg.jsf", we've introduced
103: * untestable assumptions about the local naming and navigation configurations.
104: * This method will safely return the output link value corresponding to the
105: * specified "from-outcome" view ID.
106: */
107: public static final String getActionUrl(String action) {
108: FacesContext context = FacesContext.getCurrentInstance();
109: return context.getApplication().getViewHandler().getActionURL(
110: context, action);
111: }
112:
113: /**
114: * Methods to centralize our approach to messages, since we may
115: * have to adapt the default Faces implementation.
116: */
117: public static void addErrorMessage(String message) {
118: FacesContext.getCurrentInstance().addMessage(
119: null,
120: new FacesMessage(FacesMessage.SEVERITY_ERROR, message,
121: null));
122: }
123:
124: public static void addMessage(String message) {
125: FacesContext.getCurrentInstance().addMessage(
126: null,
127: new FacesMessage(FacesMessage.SEVERITY_INFO, message,
128: null));
129: }
130:
131: public static void addUniqueErrorMessage(String message) {
132: if (!hasMessage(message)) {
133: addErrorMessage(message);
134: }
135: }
136:
137: private static boolean hasMessage(String message) {
138: for (Iterator iter = FacesContext.getCurrentInstance()
139: .getMessages(); iter.hasNext();) {
140: FacesMessage facesMessage = (FacesMessage) iter.next();
141: if (facesMessage.getSummary() != null
142: && facesMessage.getSummary().equals(message)) {
143: return true;
144: }
145: }
146: return false;
147: }
148:
149: /**
150: * We want to use standard faces messaging for intra-page messages, such
151: * as validation checking, but we want to use the custom messaging approach
152: * for inter-page messaging. So, for now we're going to add the inter-page
153: * messages to the custom MessagingBean.
154: *
155: * @param message
156: */
157: public static void addRedirectSafeMessage(String message) {
158: MessagingBean mb = (MessagingBean) resolveVariable("messagingBean");
159: // We only send informational messages across pages.
160: mb.addMessage(new FacesMessage(FacesMessage.SEVERITY_INFO,
161: message, null));
162: }
163:
164: /**
165: * JSF 1.1 provides no way to cleanly discard input fields from a table when
166: * we know we won't use them. Ideally in such circumstances we'd specify an
167: * "immediate" action handler (to skip unnecessary validation checks and
168: * model updates), and then overwrite any existing values. However,
169: * JSF absolutely insists on keeping any existing input components as
170: * they are if validation and updating hasn't been done. When the table
171: * is re-rendered, all of the readonly portions of the columns will be
172: * refreshed from the backing bean, but the input fields will
173: * keep their now-incorrect values.
174: *
175: * <p>
176: * The easiest practical way to deal with this limitation is to avoid
177: * "immediate" actions when a table contains input fields, avoid side-effects
178: * from the bogus model updates, and stick the user with the inconvenience
179: * of unnecessary validation errors.
180: *
181: * <p>
182: * The only other solution we've found is to have the backing bean bind to
183: * the data table component (which just means storing a transient
184: * pointer to the UIData or HtmlDataTable when it's passed to the
185: * bean's "setTheDataTable" method), and then to have the action handler call
186: * this method to walk the table, look for UIInputs on each row, and
187: * perform the necessary magic on each to force reloading from the data model.
188: *
189: * <p>
190: * Usage:
191: * <pre>
192: * private transient HtmlDataTable dataTable;
193: * public HtmlDataTable getDataTable() {
194: * return dataTable;
195: * }
196: * public void setDataTable(HtmlDataTable dataTable) {
197: * this.dataTable = dataTable;
198: * }
199: * public void processImmediateIdSwitch(ActionEvent event) {
200: * // ... modify the current ID ...
201: * FacesUtil.clearAllInputs(dataTable);
202: * }
203: * </pre>
204: */
205: public static void clearAllInputs(UIComponent component) {
206: if (logger.isDebugEnabled())
207: logger.debug("clearAllInputs " + component);
208: if (component instanceof UIInput) {
209: if (logger.isDebugEnabled())
210: logger
211: .debug(" setValid, setValue, setLocalValueSet, setSubmittedValue");
212: UIInput uiInput = (UIInput) component;
213: uiInput.setValid(true);
214: uiInput.setValue(null);
215: uiInput.setLocalValueSet(false);
216: uiInput.setSubmittedValue(null);
217:
218: } else if (component instanceof UIData) {
219: UIData dataTable = (UIData) component;
220: int first = dataTable.getFirst();
221: int rows = dataTable.getRows();
222: int last;
223: if (rows == 0) {
224: last = dataTable.getRowCount();
225: } else {
226: last = first + rows;
227: }
228: for (int rowIndex = first; rowIndex < last; rowIndex++) {
229: dataTable.setRowIndex(rowIndex);
230: if (dataTable.isRowAvailable()) {
231: for (Iterator iter = dataTable.getChildren()
232: .iterator(); iter.hasNext();) {
233: clearAllInputs((UIComponent) iter.next());
234: }
235: }
236: }
237: } else {
238: for (Iterator iter = component.getChildren().iterator(); iter
239: .hasNext();) {
240: clearAllInputs((UIComponent) iter.next());
241: }
242: }
243: }
244:
245: /**
246: * Gets a localized message string based on the locale determined by the
247: * FacesContext.
248: * @param key The key to look up the localized string
249: */
250: public static String getLocalizedString(FacesContext context,
251: String key) {
252: String bundleName = context.getApplication().getMessageBundle();
253: return LocaleUtil.getLocalizedString(context, bundleName, key);
254: }
255:
256: /**
257: * Gets a localized message string based on the locale determined by the
258: * FacesContext. Useful for adding localized FacesMessages from a backing bean.
259: * @param key The key to look up the localized string
260: */
261: public static String getLocalizedString(String key) {
262: return FacesUtil.getLocalizedString(FacesContext
263: .getCurrentInstance(), key);
264: }
265:
266: /**
267: * Gets a localized message string based on the locale determined by the
268: * FacesContext. Useful for adding localized FacesMessages from a backing bean.
269: *
270: *
271: * @param key The key to look up the localized string
272: * @param params The array of strings to use in replacing the placeholders
273: * in the localized string
274: */
275: public static String getLocalizedString(String key, String[] params) {
276: String rawString = getLocalizedString(key);
277: MessageFormat format = new MessageFormat(rawString);
278: return format.format(params);
279: }
280:
281: /**
282: * All Faces number formatting options round instead of truncating.
283: * For the Gradebook, virtually no displayed numbers are ever supposed to
284: * round up.
285: *
286: * This method moves the specified raw value into a higher-resolution
287: * BigDecimal, rounding away noise at MAXIMUM_MEANINGFUL_DECIMAL_PLACES.
288: * It then rounds down to reach the specified maximum number
289: * of decimal places and returns the equivalent double for
290: * further formatting.
291: *
292: * This is all necessary because we don't store scores as
293: * BigDecimal and because Java / JSF lacks a DecimalFormat
294: * class which uses "floor" instead of "round" when
295: * trimming decimal places.
296: */
297: public static double getRoundDown(double rawValue,
298: int maxDecimalPlaces) {
299: if (maxDecimalPlaces == 0) {
300: return Math.floor(rawValue);
301: } else if (rawValue != 0) {
302: // We don't use the BigDecimal ROUND_DOWN functionality directly,
303: // because moving from lower resolution storage to a higher
304: // resolution form can introduce false truncations (e.g.,
305: // a "17.99" double being treated as a "17.9899999999999"
306: // BigDecimal).
307: BigDecimal bd = (new BigDecimal(rawValue)).setScale(
308: MAXIMUM_MEANINGFUL_DECIMAL_PLACES,
309: BigDecimal.ROUND_HALF_DOWN).setScale(
310: maxDecimalPlaces, BigDecimal.ROUND_DOWN);
311:
312: if (logger.isDebugEnabled())
313: logger.debug("getRoundDown: rawValue=" + rawValue
314: + ", maxDecimalPlaces=" + maxDecimalPlaces
315: + ", bigDecimal=" + (new BigDecimal(rawValue))
316: + ", returning=" + bd.doubleValue());
317:
318: return bd.doubleValue();
319: } else {
320: return rawValue;
321: }
322: }
323: }
|