/********************************************************************************** * * $Id: FacesUtil.java 59674 2009-04-03 23:05:58Z arwhyte@umich.edu $ * *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 The Sakai Foundation, The MIT Corporation * * Licensed under the Educational Community 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.osedu.org/licenses/ECL-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 org.sakaiproject.tool.gradebook.jsf; import java.math.BigDecimal; import java.text.MessageFormat; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIData; import javax.faces.component.UIInput; import javax.faces.component.UIParameter; import javax.faces.context.FacesContext; import javax.faces.event.FacesEvent; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.jsf.util.LocaleUtil; import org.sakaiproject.tool.gradebook.ui.MessagingBean; /** * A noninstantiable utility class, because every JSF project needs one. */ public class FacesUtil { private static final Log logger = LogFactory.getLog(FacesUtil.class); /** * Before display, scores are rounded at this number of decimal * places and later truncated to (hopefully) a shorter number. */ public static int MAXIMUM_MEANINGFUL_DECIMAL_PLACES = 5; // Enforce noninstantiability. private FacesUtil() { } /** * If the JSF h:commandLink component includes f:param children, those name-value pairs * are put into the request parameter map for later use by the action handler. Unfortunately, * the same isn't done for h:commandButton. This is a workaround to let arguments * be associated with a button. * * Because action listeners are guaranteed to be executed before action methods, an * action listener can use this method to update any context the action method might need. */ public static final Map getEventParameterMap(FacesEvent event) { Map parameterMap = new HashMap(); List children = event.getComponent().getChildren(); for (Iterator iter = children.iterator(); iter.hasNext(); ) { Object next = iter.next(); if (next instanceof UIParameter) { UIParameter param = (UIParameter)next; parameterMap.put(param.getName(), param.getValue()); } } if (logger.isDebugEnabled()) logger.debug("parameterMap=" + parameterMap); return parameterMap; } /** * To cut down on configuration noise, allow access to request-scoped beans from * session-scoped beans, and so on, this method lets the caller try to find * anything anywhere that Faces can look for it. * * WARNING: If what you're looking for is a managed bean and it isn't found, * it will be created as a result of this call. */ public static final Object resolveVariable(String name) { FacesContext context = FacesContext.getCurrentInstance(); return context.getApplication().getVariableResolver().resolveVariable(context, name); } /** * Because POST arguments aren't carried over redirects, the easiest way to * get bookmarkable URLs is to use "h:outputLink" rather than "h:commandLink" or * "h:commandButton", and to add query string parameters via "f:param". However, * if the value of the output link is something like "editAsg.jsf", we've introduced * untestable assumptions about the local naming and navigation configurations. * This method will safely return the output link value corresponding to the * specified "from-outcome" view ID. */ public static final String getActionUrl(String action) { FacesContext context = FacesContext.getCurrentInstance(); return context.getApplication().getViewHandler().getActionURL(context, action); } /** * Methods to centralize our approach to messages, since we may * have to adapt the default Faces implementation. */ public static void addErrorMessage(String message) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null)); } public static void addMessage(String message) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, message, null)); } public static void addUniqueErrorMessage(String message) { if (!hasMessage(message)) { addErrorMessage(message); } } private static boolean hasMessage(String message) { for(Iterator iter = FacesContext.getCurrentInstance().getMessages(); iter.hasNext();) { FacesMessage facesMessage = (FacesMessage)iter.next(); if(facesMessage.getSummary() != null && facesMessage.getSummary().equals(message)) { return true; } } return false; } /** * We want to use standard faces messaging for intra-page messages, such * as validation checking, but we want to use the custom messaging approach * for inter-page messaging. So, for now we're going to add the inter-page * messages to the custom MessagingBean. * * @param message */ public static void addRedirectSafeMessage(String message) { MessagingBean mb = (MessagingBean)resolveVariable("messagingBean"); // We only send informational messages across pages. mb.addMessage(new FacesMessage(FacesMessage.SEVERITY_INFO, message, null)); } /** * JSF 1.1 provides no way to cleanly discard input fields from a table when * we know we won't use them. Ideally in such circumstances we'd specify an * "immediate" action handler (to skip unnecessary validation checks and * model updates), and then overwrite any existing values. However, * JSF absolutely insists on keeping any existing input components as * they are if validation and updating hasn't been done. When the table * is re-rendered, all of the readonly portions of the columns will be * refreshed from the backing bean, but the input fields will * keep their now-incorrect values. * * <p> * The easiest practical way to deal with this limitation is to avoid * "immediate" actions when a table contains input fields, avoid side-effects * from the bogus model updates, and stick the user with the inconvenience * of unnecessary validation errors. * * <p> * The only other solution we've found is to have the backing bean bind to * the data table component (which just means storing a transient * pointer to the UIData or HtmlDataTable when it's passed to the * bean's "setTheDataTable" method), and then to have the action handler call * this method to walk the table, look for UIInputs on each row, and * perform the necessary magic on each to force reloading from the data model. * * <p> * Usage: * <pre> * private transient HtmlDataTable dataTable; * public HtmlDataTable getDataTable() { * return dataTable; * } * public void setDataTable(HtmlDataTable dataTable) { * this.dataTable = dataTable; * } * public void processImmediateIdSwitch(ActionEvent event) { * // ... modify the current ID ... * FacesUtil.clearAllInputs(dataTable); * } * </pre> */ public static void clearAllInputs(UIComponent component) { if (logger.isDebugEnabled()) logger.debug("clearAllInputs " + component); if (component instanceof UIInput) { if (logger.isDebugEnabled()) logger.debug(" setValid, setValue, setLocalValueSet, setSubmittedValue"); UIInput uiInput = (UIInput)component; uiInput.setValid(true); uiInput.setValue(null); uiInput.setLocalValueSet(false); uiInput.setSubmittedValue(null); } else if (component instanceof UIData) { UIData dataTable = (UIData)component; int first = dataTable.getFirst(); int rows = dataTable.getRows(); int last; if (rows == 0) { last = dataTable.getRowCount(); } else { last = first + rows; } for (int rowIndex = first; rowIndex < last; rowIndex++) { dataTable.setRowIndex(rowIndex); if (dataTable.isRowAvailable()) { for (Iterator iter = dataTable.getChildren().iterator(); iter.hasNext(); ) { clearAllInputs((UIComponent)iter.next()); } } } } else { for (Iterator iter = component.getChildren().iterator(); iter.hasNext(); ) { clearAllInputs((UIComponent)iter.next()); } } } /** * Gets a localized message string based on the locale determined by the * FacesContext. * @param key The key to look up the localized string */ public static String getLocalizedString(FacesContext context, String key) { String bundleName = context.getApplication().getMessageBundle(); return LocaleUtil.getLocalizedString(context, bundleName, key); } /** * Gets a localized message string based on the locale determined by the * FacesContext. Useful for adding localized FacesMessages from a backing bean. * @param key The key to look up the localized string */ public static String getLocalizedString(String key) { return FacesUtil.getLocalizedString(FacesContext.getCurrentInstance(), key); } /** * Gets a localized message string based on the locale determined by the * FacesContext. Useful for adding localized FacesMessages from a backing bean. * * * @param key The key to look up the localized string * @param params The array of strings to use in replacing the placeholders * in the localized string */ public static String getLocalizedString(String key, String[] params) { String rawString = getLocalizedString(key); MessageFormat format = new MessageFormat(rawString); return format.format(params); } /** * All Faces number formatting options round instead of truncating. * For the Gradebook, virtually no displayed numbers are ever supposed to * round up. * * This method moves the specified raw value into a higher-resolution * BigDecimal, rounding away noise at MAXIMUM_MEANINGFUL_DECIMAL_PLACES. * It then rounds down to reach the specified maximum number * of decimal places and returns the equivalent double for * further formatting. * * This is all necessary because we don't store scores as * BigDecimal and because Java / JSF lacks a DecimalFormat * class which uses "floor" instead of "round" when * trimming decimal places. */ public static double getRoundDown(double rawValue, int maxDecimalPlaces) { if (maxDecimalPlaces == 0) { return Math.floor(rawValue); } else if (rawValue != 0) { // We don't use the BigDecimal ROUND_DOWN functionality directly, // because moving from lower resolution storage to a higher // resolution form can introduce false truncations (e.g., // a "17.99" double being treated as a "17.9899999999999" // BigDecimal). BigDecimal bd = (new BigDecimal(rawValue)). setScale(MAXIMUM_MEANINGFUL_DECIMAL_PLACES, BigDecimal.ROUND_HALF_DOWN). setScale(maxDecimalPlaces, BigDecimal.ROUND_DOWN); if (logger.isDebugEnabled()) logger.debug("getRoundDown: rawValue=" + rawValue + ", maxDecimalPlaces=" + maxDecimalPlaces + ", bigDecimal=" + (new BigDecimal(rawValue)) + ", returning=" + bd.doubleValue()); return bd.doubleValue(); } else { return rawValue; } } }