001: /*
002: * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or
005: * without modification, are permitted provided that the following
006: * conditions are met:
007: *
008: * - Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: *
011: * - Redistribution in binary form must reproduce the above
012: * copyright notice, this list of conditions and the following
013: * disclaimer in the documentation and/or other materials
014: * provided with the distribution.
015: *
016: * Neither the name of Sun Microsystems, Inc. or the names of
017: * contributors may be used to endorse or promote products derived
018: * from this software without specific prior written permission.
019: *
020: * This software is provided "AS IS," without a warranty of any
021: * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
022: * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
023: * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
024: * EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY
025: * DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT OF OR
026: * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THIS SOFTWARE OR
027: * ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE
028: * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT,
029: * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
030: * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF
031: * THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS
032: * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
033: *
034: * You acknowledge that this software is not designed, licensed or
035: * intended for use in the design, construction, operation or
036: * maintenance of any nuclear facility.
037: */
038:
039: package org.apache.cocoon.faces.samples.carstore;
040:
041: import org.apache.commons.logging.Log;
042: import org.apache.commons.logging.LogFactory;
043:
044: import javax.faces.application.Application;
045: import javax.faces.application.FacesMessage;
046: import javax.faces.component.UIComponent;
047: import javax.faces.component.UIInput;
048: import javax.faces.component.UISelectItems;
049: import javax.faces.component.ValueHolder;
050: import javax.faces.context.FacesContext;
051: import javax.faces.convert.Converter;
052: import javax.faces.model.SelectItem;
053:
054: import java.util.ArrayList;
055: import java.util.Enumeration;
056: import java.util.HashMap;
057: import java.util.Iterator;
058: import java.util.Map;
059: import java.util.MissingResourceException;
060: import java.util.ResourceBundle;
061: import java.util.StringTokenizer;
062:
063: /**
064: * <p>This bean encapsulates a car model, including pricing and package
065: * choices. The system allows the user to customize the properties of
066: * this bean with the help of the {@link CarCustomizer}.</p>
067: *
068: * <h3>Data Access</h3>
069: *
070: * <p>This is the only bean in the system that has complicated access to
071: * the persistent store of data. In the present implementation, this
072: * persistent store is in <code>ResourceBundle</code> instances.</p>
073: *
074: * <p>There are three data source <code>ResourceBundle</code> files
075: * used:</p>
076: *
077: * <ol>
078: *
079: * <li><p><code><ModelName></code></p>
080: *
081: * <p>This contains the localized content for this model. There
082: * is a variant of this file for each supported locale, for
083: * example, <code>Jalopy_de.properties</code></p>
084: *
085: * </li>
086: *
087: * <li><p><code><Common_properties></code></p>
088: *
089: * <p>This contains the localized content common to all
090: * models.</p>
091: *
092: * </li>
093: *
094: * <li><p><code><ModelName_options></code></p>
095: *
096: * <p>This contains the non-localized content for this model,
097: * including the non-localized options. There is only one
098: * variant of this file for all locales for example,
099: * <code>Jalopy_options.properties</code></p>
100: *
101: * </li>
102: *
103: * </ol>
104: *
105: * <p>All files conform to the following convention:</p>
106: *
107: * <code><pre>
108: * key
109: * key_componentType
110: * key_valueType
111: * </pre></code>
112: *
113: * <p>Where <code>key</code> is the name of an attribute of this car.
114: * For example, <code>basePrice</code>, or <code>description</code>.
115: * <code>key_componentType</code> is the component type of the
116: * <code>UIComponent</code> subclass to be used to represent this
117: * attribute in the page, for example <code>SelectManyMenu</code>.
118: * <code>key_valueType</code> is the data type of the value of the
119: * <code>UIComponent</code>, for example <code>java.lang.Integer</code>.
120: * For all non-String valueTypes.</p>
121: *
122: * <p>When the bean is instantiated, we load both of the above
123: * properties files and iterate over the keys in each one. For each
124: * key, we look at the <code>componentType</code> and ask the
125: * <code>Application</code> to create a component of that type. We
126: * store that <code>UIComponent</code> instance in our
127: * <code>components</code> <code>Map</code> under the name
128: * <code>key</code>. We look at the <code>valueType</code> for the
129: * <code>key</code>. For non <code>java.lang.String</code> types, we
130: * ask the <code>Application</code> for a <code>Converter</code>
131: * instance for that class. If found, we use it to convert the value
132: * for the <code>key</code> to the appropriate type and store that as
133: * the <code>value</code> of the <code>UIComponent</code> instance.</p>
134: */
135:
136: public class CarBean extends Object {
137:
138: protected static final Log log = LogFactory.getLog(CarBean.class);
139:
140: /**
141: * <p>The message identifier of the Message to be created if
142: * the conversion fails. The message format string for this
143: * message may optionally include a <code>{0}</code>
144: * placeholder, which will be replaced by the object and value.</p>
145: */
146: public static final String CONVERTER_ERROR_MESSAGE_ID = "carstore.Converter_Error";
147:
148: //
149: // Relationship Instance Variables
150: //
151:
152: /**
153: * Localized labels
154: */
155:
156: private ResourceBundle resources = null;
157:
158: /**
159: * Price data
160: */
161: private ResourceBundle priceData = null;
162:
163: /**
164: * Keys: String attribute name, such as engine. Values: UIComponent
165: * for the attribute
166: */
167:
168: private Map components = null;
169:
170: /**
171: * Keys: String attribute name, such as engine. Values: String value
172: * of the component named by key in our components Map.
173: */
174:
175: private Map attributes = null;
176:
177: //
178: // Constructors
179: //
180:
181: public CarBean() {
182: this .init(CarStore.DEFAULT_MODEL_PROPERTIES);
183: }
184:
185: public CarBean(String bundleName) {
186: this .init(bundleName);
187: }
188:
189: /**
190: * <p>Initialize our components <code>Map</code> as described in the
191: * class documentation.</p>
192: *
193: * <p>Create a wrapper <code>Map</code> around the components
194: * <code>Map</code> that exposes the String converted value of each
195: * component.</p>
196: */
197:
198: private void init(String bundleName) {
199: FacesContext context = FacesContext.getCurrentInstance();
200: ResourceBundle data = null;
201: Enumeration keys = null;
202: components = new HashMap();
203:
204: // load the labels
205: resources = ResourceBundle.getBundle(CarStore.CARSTORE_PREFIX
206: + ".bundles.Resources", context.getViewRoot()
207: .getLocale());
208:
209: // load the prices
210: priceData = ResourceBundle.getBundle(CarStore.CARSTORE_PREFIX
211: + ".bundles.OptionPrices");
212:
213: // populate the locale-specific information
214: if (log.isDebugEnabled()) {
215: log.debug("Loading bundle: " + bundleName + ".");
216: }
217: data = ResourceBundle.getBundle(bundleName, context
218: .getViewRoot().getLocale());
219: if (log.isDebugEnabled()) {
220: log.debug("Bundle " + bundleName
221: + " loaded. Reading properties...");
222: }
223: initComponentsFromProperties(context, data);
224: if (log.isDebugEnabled()) {
225: log.debug("done.");
226: }
227:
228: // populate the non-locale-specific information common to all cars
229: if (log.isDebugEnabled()) {
230: log.debug("Loading bundle: Common_options.");
231: }
232: data = ResourceBundle.getBundle(CarStore.CARSTORE_PREFIX
233: + ".bundles.Common_options");
234: if (log.isDebugEnabled()) {
235: log
236: .debug("Bundle Common_options loaded. Reading properties...");
237: }
238: initComponentsFromProperties(context, data);
239: if (log.isDebugEnabled()) {
240: log.debug("done.");
241: }
242:
243: // populate the non-locale-specific information specific to each car
244: if (log.isDebugEnabled()) {
245: log.debug("Loading bundle: " + bundleName + "_options.");
246: }
247: data = ResourceBundle.getBundle(bundleName + "_options");
248: if (log.isDebugEnabled()) {
249: log.debug("Bundle " + bundleName
250: + "_options loaded. Reading properties...");
251: }
252: initComponentsFromProperties(context, data);
253: if (log.isDebugEnabled()) {
254: log.debug("done.");
255: }
256:
257: // create a read-only Map exposing the values of all of our
258: // components.
259: attributes = new Map() {
260: public void clear() {
261: CarBean.this .components.clear();
262: }
263:
264: public boolean containsKey(Object key) {
265: return CarBean.this .components.containsKey(key);
266: }
267:
268: public boolean containsValue(Object value) {
269: throw new UnsupportedOperationException();
270: }
271:
272: public java.util.Set entrySet() {
273: throw new UnsupportedOperationException();
274: }
275:
276: public boolean equals(Object o) {
277: throw new UnsupportedOperationException();
278: }
279:
280: public Object get(Object key) {
281: UIComponent component = null;
282: Converter converter = null;
283: Object result = null;
284: if (null == key) {
285: return null;
286: }
287: if (null != (component = (UIComponent) CarBean.this .components
288: .get(key))) {
289: // if this component can have a Converter
290: if (component instanceof ValueHolder) {
291: // try to get it
292: converter = ((ValueHolder) component)
293: .getConverter();
294: result = ((ValueHolder) component).getValue();
295: }
296:
297: // if we do have a value
298: if (null != result) {
299: // and we do have a converter
300: if (null != converter) {
301: // convert the value to String
302: result = converter.getAsString(FacesContext
303: .getCurrentInstance(), component,
304: result);
305: }
306: }
307: }
308: return result;
309: }
310:
311: public int hashCode() {
312: return CarBean.this .components.hashCode();
313: }
314:
315: public boolean isEmpty() {
316: return CarBean.this .components.isEmpty();
317: }
318:
319: public java.util.Set keySet() {
320: return CarBean.this .components.keySet();
321: }
322:
323: public Object put(Object k, Object v) {
324: throw new UnsupportedOperationException();
325: }
326:
327: public void putAll(Map t) {
328: throw new UnsupportedOperationException();
329: }
330:
331: public Object remove(Object k) {
332: throw new UnsupportedOperationException();
333: }
334:
335: public int size() {
336: return CarBean.this .components.size();
337: }
338:
339: public java.util.Collection values() {
340: ArrayList result = new ArrayList();
341: Iterator keys = keySet().iterator();
342: while (keys.hasNext()) {
343: result.add(get(keys.next()));
344: }
345: return result;
346: }
347: };
348:
349: }
350:
351: /**
352: * <p>For each entry in data, create component and cause it to be
353: * populated with values.</p>
354: */
355:
356: private void initComponentsFromProperties(FacesContext context,
357: ResourceBundle data) {
358: Application application = context.getApplication();
359: Enumeration keys = data.getKeys();
360: String key = null, value = null, componentType = null, valueType = null;
361: UIComponent component = null;
362:
363: // populate the map
364: while (keys.hasMoreElements()) {
365: key = (String) keys.nextElement();
366: if (key == null) {
367: continue;
368: }
369: // skip secondary keys.
370: if (-1 != key.indexOf("_")) {
371: continue;
372: }
373: value = data.getString(key);
374: componentType = data.getString(key + "_componentType");
375: valueType = data.getString(key + "_valueType");
376: if (log.isDebugEnabled()) {
377: log
378: .debug("populating map for " + key + "\n"
379: + "\n\tvalue: " + value
380: + "\n\tcomponentType: " + componentType
381: + "\n\tvalueType: " + valueType);
382: }
383: // create the component for this componentType
384: component = application.createComponent(componentType);
385: populateComponentWithValue(context, component,
386: componentType, value, valueType);
387: components.put(key, component);
388: }
389: }
390:
391: /**
392: * <p>populate the argument component with values, being sensitive
393: * to the possible multi-nature of the values, and to the type of
394: * the values.</p>
395: */
396:
397: private void populateComponentWithValue(FacesContext context,
398: UIComponent component, String componentType, String value,
399: String valueType) {
400: Application application = context.getApplication();
401: Converter converter = null;
402: UISelectItems items = null;
403:
404: // if we need a converter, and can have a converter
405: if (!valueType.equals("java.lang.String")
406: && component instanceof ValueHolder) {
407: // if so create it,
408: try {
409: converter = application.createConverter(CarStore
410: .loadClass(valueType, this ));
411: } catch (ClassNotFoundException cne) {
412: FacesMessage errMsg = MessageFactory.getMessage(
413: CONVERTER_ERROR_MESSAGE_ID,
414: (new Object[] { valueType }));
415: throw new IllegalStateException(errMsg.getSummary());
416: }
417: // add it to our component,
418: ((ValueHolder) component).setConverter(converter);
419: }
420:
421: // if this component is a SelectOne or SelectMany, take special action
422: if (isMultiValue(componentType)) {
423: // create a UISelectItems instance
424: items = new UISelectItems();
425: items.setValue(parseStringIntoArrayList(context, component,
426: value, valueType, converter));
427: // add it to the component
428: component.getChildren().add(items);
429: } else {
430: // we have a single value
431: if (null != converter) {
432: component.getAttributes()
433: .put(
434: "value",
435: converter.getAsObject(context,
436: component, value));
437: } else {
438: component.getAttributes().put("value", value);
439: }
440: }
441: }
442:
443: /**
444: * @return true if componentType starts with SelectMany or SelectOne
445: */
446: private boolean isMultiValue(String componentType) {
447: if (null == componentType) {
448: return false;
449: }
450: return (componentType.startsWith("javax.faces.SelectMany") || componentType
451: .startsWith("javax.faces.SelectOne"));
452: }
453:
454: /**
455: * Tokenizes the passed in String which is a comma separated string of
456: * option values that serve as keys into the main resources file.
457: * For example, optionStr could be "Disc,Drum", which corresponds to
458: * brake options available for the chosen car. This String is tokenized
459: * and used as key into the main resource file to get the localized option
460: * values and stored in the passed in ArrayList.
461: */
462: public ArrayList parseStringIntoArrayList(FacesContext context,
463: UIComponent component, String value, String valueType,
464: Converter converter) {
465: ArrayList optionsList = null;
466: int i = 0;
467: Object optionValue = null;
468: String optionKey = null, optionLabel = null;
469: Map nonLocalizedOptionValues = null;
470:
471: if (value == null) {
472: return null;
473: }
474: StringTokenizer st = new StringTokenizer(value, ",");
475: optionsList = new ArrayList((st.countTokens()) + 1);
476: while (st.hasMoreTokens()) {
477: optionKey = st.nextToken();
478: try {
479: optionLabel = resources.getString(optionKey);
480: } catch (MissingResourceException e) {
481: // if we can't find a hit, the key is the label
482: optionLabel = optionKey;
483: }
484:
485: if (null != converter) {
486: // PENDING deal with the converter case
487: } else {
488: optionsList.add(new SelectItem(optionKey, optionLabel));
489: }
490: }
491: return optionsList;
492: }
493:
494: public String updatePricing() {
495: getCurrentPrice();
496: return null;
497: }
498:
499: public Integer getCurrentPrice() {
500: // go through our options and try to get the prices
501: int sum = ((Integer) ((ValueHolder) getComponents().get(
502: "basePrice")).getValue()).intValue();
503: Iterator iter = getComponents().keySet().iterator();
504: String key = null;
505: Object value = null;
506: UIComponent component = null;
507: while (iter.hasNext()) {
508: key = (String) iter.next();
509: component = (UIComponent) getComponents().get(key);
510: value = component.getAttributes().get("value");
511: if (null == value || (!(component instanceof UIInput))) {
512: continue;
513: }
514:
515: // if the value is a String, see if we have priceData for it
516: if (value instanceof String) {
517: try {
518: sum += Integer.valueOf(
519: priceData.getString((String) value))
520: .intValue();
521: } catch (NumberFormatException e) {
522: }
523: }
524: // if the value is a Boolean, look up the price by name
525: else if (value instanceof Boolean
526: && ((Boolean) value).booleanValue()) {
527: try {
528: sum += Integer.valueOf(priceData.getString(key))
529: .intValue();
530: } catch (NumberFormatException e) {
531: }
532: } else if (value instanceof Number) {
533: sum += ((Number) value).intValue();
534: }
535: }
536: Integer result = new Integer(sum);
537: // store the new price into the component for currentPrice
538: ((ValueHolder) getComponents().get("currentPrice"))
539: .setValue(result);
540: return result;
541: }
542:
543: public Map getComponents() {
544: return components;
545: }
546:
547: public Map getAttributes() {
548: return attributes;
549: }
550:
551: }
|