Encoding JavaScript to Avoid Server Roundtrips

Revisiting the Spinner

Next, we revisit the spinner listed in the previous section. That spinner has two serious drawbacks. First, the spinner component renders itself, so you could not, for example, attach a separate renderer to the spinner when you migrate your application to cell phones.

Second, the spinner requires a roundtrip to the server every time a user clicks the increment or decrement button. Nobody would implement an industrial-strength spinner with those deficiencies. Now we see how to address them.

While we are at it, we will also add another feature to the spinner the ability to attach value change listeners.

Using an External Renderer

In the preceding example, the UISpinner class was in charge of its own rendering. However, most UI classes delegate rendering to a separate class. Using separate renderers is a good idea: It becomes easy to replace renderers, to adapt to a different UI toolkit, or simply to achieve different HTML effects.

In "Encoding JavaScript to Avoid Server Roundtrips" on page 404 we see how to use an alternative renderer that uses JavaScript to keep track of the spinner's value on the client.

Using an external renderer requires these steps:

1.

Define an ID string for your renderer.

2.

Declare the renderer in a JSF configuration file.

3.

Modify your tag class to return the renderer's ID from the getRendererType method.

4.

Implement the renderer class.

The identifier in our case, com.corejsf.Spinner must be defined in a JSF configuration file, like this:

  <faces-config>     ...     <component>        <component-type>com.corejsf.Spinner</component-type>        <component-class>com.corejsf.UISpinner</component-class>     </component>     <render-kit>        <renderer>           <component-family>javax.faces.Input</component-family>           <renderer-type>com.corejsf.Spinner</renderer-type>           <renderer-class>com.corejsf.SpinnerRenderer</renderer-class>        </renderer>      </render-kit>   </faces-config>

The component-family element serves to overcome a historical problem. The names of the standard HTML tags are meant to indicate the component type and the renderer type. For example, an h:selectOneMenu is a UISelectOne component whose renderer has type javax.faces.Menu. That same renderer can also be used for the h:selectManyMenu tag. But the scheme did not work so well. The renderer for h:inputText writes an HTML input text field. That renderer will not work for h:outputText you do not want to use a text field for output.

So, instead of identifying renderers by individual components, renderers are determined by the renderer type and the component family. Table 9-2 shows the component families of all standard component classes. In our case, we use the component family javax.faces.Input because UISpinner is a subclass of UIInput.

Table 9-2. Component Families of Standard Component Classes
Component Class Component Family
UICommand javax.faces.Command
UIData javax.faces.Data
UIForm javax.faces.Form
UIGraphic javax.faces.Graphic
UIInput javax.faces.Input
UIMessage javax.faces.Message
UIMessages javax.faces.Messages
UIOutput javax.faces.Output
UIPanel javax.faces.Panel
UISelectBoolean javax.faces.SelectBoolean
UISelectMany javax.faces.SelectMany
UISelectOne javax.faces.SelectOne


The getRendererType of your tag class needs to return the renderer ID.

  public class SpinnerTag extends UIComponentTag {      ...      public String getComponentType() { return "com.corejsf.Spinner"; }      public String getRendererType()  { return "com.corejsf.Spinner"; }      ...   }

Note

Component IDs and renderer IDs have separate name spaces. It is okay to use the same string as a component ID and a renderer ID.


It is also a good idea to set the renderer type in the component constructor:

  public class UISpinner extends UIInput {      public UISpinner() {         setConverter(new IntegerConverter()); // to convert the submitted value         setRendererType("com.corejsf.Spinner"); // this component has a renderer      }   }     

Then the renderer type is properly set if a component is used programmatically, without the use of tags.

The final step is implementing the renderer itself. Renderers extend the javax.faces.render.Renderer class. That class has seven methods, four of which are familiar:

  • void encodeBegin(FacesContext context, UIComponent component)

  • void encodeChildren(FacesContext context, UIComponent component)

  • void encodeEnd(FacesContext context, UIComponent component)

  • void decode(FacesContext context, UIComponent component)

The renderer methods listed above are almost identical to their component counterparts except that the renderer methods take an additional argument: a reference to the component being rendered. To implement those methods for the spinner renderer, we move the component methods to the renderer and apply code changes to compensate for the fact that the renderer is passed a reference to the component. That is easy to do.

Here are the remaining renderer methods:

  • Object getConvertedValue(FacesContext context, UIComponent component,    Object submittedValue)

  • boolean getRendersChildren()

  • String convertClientId(FacesContext context, String clientId)

The getConvertedValue method converts a component's submitted value from a string to an object. The default implementation in the Renderer class returns the value.

The getRendersChildren method specifies whether a renderer is responsible for rendering its component's children. If that method returns true, JSF will call the renderer's encodeChildren method; if it returns false (the default behavior), the JSF implementation will not call that method and the children will be encoded separately.

The convertClientId method converts an ID string (such as _id1:monthSpinner) so that it can be used on the client some clients may place restrictions on IDs, such as disallowing special characters. However, the default implementation returns the ID string, unchanged.

If you have a component that renders itself, it is usually a simple task to move code from the component to the renderer. Listing 9-10 and Listing 9-11 show the code for the spinner component and the renderer, respectively.

Listing 9-10. spinner2/src/java/com/corejsf/UISpinner.java

  1. package com.corejsf;   2.   3. import javax.faces.component.UIInput;   4. import javax.faces.convert.IntegerConverter;   5.   6. public class UISpinner extends UIInput {   7.    public UISpinner() {   8.       setConverter(new IntegerConverter()); // to convert the submitted value   9.    }  10. }     

Listing 9-11. spinner2/src/java/com/corejsf/SpinnerRenderer.java

  1. package com.corejsf;   2.   3. import java.io.IOException;   4. import java.util.Map;   5. import javax.faces.component.UIComponent;   6. import javax.faces.component.EditableValueHolder;   7. import javax.faces.component.UIInput;   8. import javax.faces.context.FacesContext;   9. import javax.faces.context.ResponseWriter;  10. import javax.faces.convert.ConverterException;  11. import javax.faces.render.Renderer;  12.  13. public class SpinnerRenderer extends Renderer {  14.    private static final String MORE = ".more";  15.    private static final String LESS = ".less";  16.  17.    public Object getConvertedValue(FacesContext context, UIComponent component,  18.          Object submittedValue) throws ConverterException {  19.       return com.corejsf.util.Renderers.getConvertedValue(context, component,  20.          submittedValue);  21.    }  22.  23.    public void encodeBegin(FacesContext context, UIComponent spinner)  24.          throws IOException {  25.       ResponseWriter writer = context.getResponseWriter();  26.       String clientId = spinner.getClientId(context);  27.  28.       encodeInputField(spinner, writer, clientId);  29.       encodeDecrementButton(spinner, writer, clientId);  30.       encodeIncrementButton(spinner, writer, clientId);  31.    }  32.  33.    public void decode(FacesContext context, UIComponent component) {  34.       EditableValueHolder spinner = (EditableValueHolder) component;  35.       Map<String, String> requestMap  36.          = context.getExternalContext().getRequestParameterMap();  37.       String clientId = component.getClientId(context);  38.  39.       int increment;  40.       if (requestMap.containsKey(clientId + MORE)) increment = 1;  41.       else if (requestMap.containsKey(clientId + LESS)) increment = -1;  42.       else increment = 0;  43.  44.       try {  45.          int submittedValue  46.             = Integer.parseInt((String) requestMap.get(clientId));  47.  48.          int newValue = getIncrementedValue(component, submittedValue,  49.             increment);  50.          spinner.setSubmittedValue("" + newValue);  51.          spinner.setValid(true);  52.       }  53.       catch(NumberFormatException ex) {  54.          // let the converter take care of bad input, but we still have  55.          // to set the submitted value, or the converter won't have  56.          // any input to deal with  57.          spinner.setSubmittedValue((String) requestMap.get(clientId));  58.       }  59.     }  60.  61.    private void encodeInputField(UIComponent spinner, ResponseWriter writer,  62.          String clientId) throws IOException {  63.       writer.startElement("input", spinner);  64.       writer.writeAttribute("name", clientId, "clientId");  65.  66.       Object v = ((UIInput) spinner).getValue();  67.       if(v != null)  68.          writer.writeAttribute("value", v.toString(), "value");  69.  70.       Integer size = (Integer) spinner.getAttributes().get("size");  71.       if(size != null)  72.          writer.writeAttribute("size", size, "size");  73.  74.       writer.endElement("input");  75.     }  76.  77.    private void encodeDecrementButton(UIComponent spinner,  78.          ResponseWriter writer, String clientId) throws IOException {  79.       writer.startElement("input", spinner);  80.       writer.writeAttribute("type", "submit", null);  81.       writer.writeAttribute("name", clientId + LESS, null);  82.       writer.writeAttribute("value", "<", "value");  83.       writer.endElement("input");  84.     }  85.  86.    private void encodeIncrementButton(UIComponent spinner,  87.          ResponseWriter writer, String clientId) throws IOException {  88.       writer.startElement("input", spinner);  89.       writer.writeAttribute("type", "submit", null);  90.       writer.writeAttribute("name", clientId + MORE, null);  91.       writer.writeAttribute("value", ">", "value");  92.       writer.endElement("input");  93.     }  94.  95.    private int getIncrementedValue(UIComponent spinner, int submittedValue,  96.          int increment) {  97.       Integer minimum = (Integer) spinner.getAttributes().get("minimum");  98.       Integer maximum = (Integer) spinner.getAttributes().get("maximum");  99.       int newValue = submittedValue + increment; 100. 101.       if ((minimum == null || newValue >= minimum.intValue()) && 102.          (maximum == null || newValue <= maximum.intValue())) 103.          return newValue; 104.       else 105.          return submittedValue; 106.     } 107. }     

Calling Converters from External Renderers

If you compare Listing 9-10 and Listing 9-11 with Listing 9-1, you will see that we moved most of the code from the original component class to a new renderer class.

However, there is a hitch. As you can see from Listing 9-10, the spinner handles conversions simply by invoking setConverter() in its constructor. Because the spinner is an input component, its superclass UIInput uses the specified converter during the Process Validations phase of the life cycle.

But when the spinner delegates to a renderer, it is the renderer's responsibility to convert the spinner's value by overriding Renderer.getConvertedValue(). So we must replicate the conversion code from UIInput in a custom renderer. We placed that code which is required in all renderers that use a converter in the static getConvertedValue method of the class com.corejsf.util.Renderers (see Listing 9-12 on page 398).

Note

The Renderers.getConvertedValue method shown in Listing 9-12 is a necessary evil because UIInput does not make its conversion code publicly available. That code resides in the protected UIInput.getConvertedValue method, which looks like this in the JSF 1.2 Reference Implementation:

// This code is from the javax.faces.component.UIInput class: public void getConvertedValue(FacesContext context, Object newSubmittedValue)       throws ConverterException {    Object newValue = newSubmittedValue;    if (renderer != null) {       newValue = renderer.getConvertedValue(context, this, newSubmittedValue);    } else if (newSubmittedValue instanceof String) {       Converter converter = getConverterWithType(context); // a private method       if (converter != null)         newValue = converter.getAsObject(            context, this, (String) newSubmittedValue);    }    return newValue; }     

The private getConverterWithType method looks up the appropriate converter for the component value.

Because UIInput's conversion code is buried in protected and private methods, it is not available for a renderer to reuse. Custom components that use converters must duplicate the code see, for example, the implementation of com.sun.faces.renderkit.html_basic.HtmlBasicInputRenderer in the reference implementation. Our com.corejsf.util.Renderers class provides the code for use in your own classes.


Supporting Value Change Listeners

If your custom component is an input component, you can fire value change events to interested listeners. For example, in a calendar application, you may want to update another component whenever a month spinner value changes.

Fortunately, it is easy to support value change listeners. The UIInput class automatically generates value change events whenever the input value has changed. Recall that there are two ways of attaching a value change listener. You can add one or more listeners with f:valueChangeListener, like this:

  <corejsf:spinner ...>      <f:valueChangeListener type="com.corejsf.SpinnerListener"/>      ...   </corejsf:spinner>

Or you can use a valueChangeListener attribute:

  <corejsf:spinner value="#{cardExpirationDate.month}"       minimum="1" maximum="12" size="3"      valueChangeListener="#{cardExpirationDate.changeListener}"/>

The first way doesn't require any effort on the part of the component implementor. The second way merely requires that your tag handler supports the valueChangeListener attribute. The attribute value is a method expression that requires special handling the topic of the next section, "Supporting Method Expressions."

In the sample program, we demonstrate the value change listener by keeping a count of all value changes that we display on the form (see Figure 9-7).

Figure 9-7. Counting the value changes


   public class CreditCardExpiration {       private int changes = 0;       // to demonstrate the value change listener       public void changeListener(ValueChangeEvent e) {          changes++;       }    }

Supporting Method Expressions

Four commonly used attributes require method expressions (see Table 9-3). You declare them in the TLD file with deferred-method elements, such as the following:

  <attribute>      <name>valueChangeListener</name>      <deferred-method>         <method-signature>            void valueChange(javax.faces.event.ValueChangeEvent)         </method-signature>      </deferred-method>   </attribute>

In the tag handler class, you provide setters for MethodExpression objects.

  public class SpinnerTag extends UIComponentELTag {      ...      private MethodExpression valueChangeListener = null;      public void setValueChangeListener(MethodExpression newValue)  {         valueChangeListener = newValue;      }      ...   }

Table 9-3. Processing Method Expressions
Attribute Name method-signature Element in TLD Code in setProperties Method

valueChangeListener

void valueChange(javax.faces. event.ValueChangeEvent)

((EditableValueHolder) component) .addValueChangeListener(new MethodExpressionValueChangeListener(expr));

validator

void validate(javax.faces. context.FacesContext, javax.faces.component. UIComponent, java.lang.Object)

((EditableValueHolder) component) .addValidator(new MethodExpressionValidator(expr));

actionListener

void actionListener(javax. faces.event.ActionEvent)

((ActionSource) component) .addActionListener(new MethodExpressionActionListener(expr));

action

java.lang.Object action()

((ActionSource2) component). addAction(expr);


In the setProperties method of the tag handler, you convert the MethodExpression object to an appropriate listener object and add it to the component:

  public void setProperties(UIComponent component) {      super.setProperties(component);      ...      if (valueChangeListener != null)         ((EditableValueHolder) component).addValueChangeListener(               new MethodExpressionValueChangeListener(valueChangeListener));   }     

Table 9-3 shows how to handle the other method attributes.

Note

The action attribute value can be either a method expression or a constant. In the latter case, a method is created that always returns the constant value.


The Sample Application

Figure 9-8 shows the directory structure of the sample application. As in the first example, we rely on the core JSF Renderers convenience class that contains the code for invoking the converter.

Figure 9-8. Directory structure of the revisited spinner example


(The Renderers class also contains a getSelectedItems method that we need later in this chapter ignore it for now.) Listing 9-13 contains the revised SpinnerTag class, and Listing 9-14 shows the faces-config.xml file.

Listing 9-12. spinner2/src/java/com/corejsf/util/Renderers.java

  1. package com.corejsf.util;   2.   3. import java.util.ArrayList;   4. import java.util.Arrays;   5. import java.util.Collection;   6. import java.util.List;   7. import java.util.Map;   8.   9. import javax.el.ValueExpression;  10. import javax.faces.application.Application;  11. import javax.faces.component.UIComponent;  12. import javax.faces.component.UIForm;  13. import javax.faces.component.UISelectItem;  14. import javax.faces.component.UISelectItems;  15. import javax.faces.component.ValueHolder;  16. import javax.faces.context.FacesContext;  17. import javax.faces.convert.Converter;  18. import javax.faces.convert.ConverterException;  19. import javax.faces.model.SelectItem;  20.  21. public class Renderers {  22.    public static Object getConvertedValue(FacesContext context,  23.          UIComponent component, Object submittedValue)  24.          throws ConverterException {  25.       if (submittedValue instanceof String) {  26.          Converter converter = getConverter(context, component);  27.          if (converter != null) {  28.             return converter.getAsObject(context, component,  29.                   (String) submittedValue);  30.          }  31.       }  32.       return submittedValue;  33.    }  34.  35.    public static Converter getConverter(FacesContext context,  36.          UIComponent component) {  37.       if (!(component instanceof ValueHolder)) return null;  38.       ValueHolder holder = (ValueHolder) component;  39.  40.       Converter converter = holder.getConverter();  41.       if (converter != null)  42.          return converter;  43.  44.       ValueExpression expr = component.getValueExpression("value");  45.       if (expr == null) return null;  46.  47.       Class targetType = expr.getType(context.getELContext());  48.       if (targetType == null) return null;  49.       // Version 1.0 of the reference implementation will not apply a converter  50.       // if the target type is String or Object, but that is a bug.  51.  52.       Application app = context.getApplication();  53.       return app.createConverter(targetType);  54.    }  55.  56.    public static String getFormId(FacesContext context, UIComponent component) {  57.       UIComponent parent = component;  58.       while (!(parent instanceof UIForm))  59.          parent = parent.getParent();  60.       return parent.getClientId(context);  61.    }  62.  63.    @SuppressWarnings("unchecked")  64.    public static List<SelectItem> getSelectItems(UIComponent component) {  65.       ArrayList<SelectItem> list = new ArrayList<SelectItem>();  66.       for (UIComponent child : component.getChildren()) {  67.          if (child instanceof UISelectItem) {  68.             Object value = ((UISelectItem) child).getValue();  69.             if (value == null) {  70.                UISelectItem item = (UISelectItem) child;  71.                list.add(new SelectItem(item.getItemValue(),  72.                      item.getItemLabel(),  73.                      item.getItemDescription(),  74.                      item.isItemDisabled()));  75.             } else if (value instanceof SelectItem) {  76.                list.add((SelectItem) value);  77.             }  78.          } else if (child instanceof UISelectItems) {  79.             Object value = ((UISelectItems) child).getValue();  80.             if (value instanceof SelectItem)  81.                list.add((SelectItem) value);  82.             else if (value instanceof SelectItem[])  83.                list.addAll(Arrays.asList((SelectItem[]) value));  84.             else if (value instanceof Collection)  85.                list.addAll((Collection<SelectItem>) value); // unavoidable  86.             // warning  87.             else if (value instanceof Map) {  88.                for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())  89.                   list.add(new SelectItem(entry.getKey(),  90.                         "" + entry.getValue()));  91.             }  92.          }  93.       }  94.       return list;  95.    }  96. }     

Listing 9-13. spinner2/src/java/com/corejsf/SpinnerTag.java

  1. package com.corejsf;   2.   3. import javax.el.MethodExpression;   4. import javax.el.ValueExpression;   5. import javax.faces.component.EditableValueHolder;   6. import javax.faces.component.UIComponent;   7. import javax.faces.event.MethodExpressionValueChangeListener;   8. import javax.faces.webapp.UIComponentELTag;   9.  10. public class SpinnerTag extends UIComponentELTag {  11.    private ValueExpression minimum = null;  12.    private ValueExpression maximum = null;  13.    private ValueExpression size = null;  14.    private ValueExpression value = null;  15.    private MethodExpression valueChangeListener = null;  16.  17.    public String getRendererType() { return "com.corejsf.Spinner"; }  18.    public String getComponentType() { return "com.corejsf.Spinner"; }  19.  20.    public void setMinimum(ValueExpression newValue) { minimum = newValue; }  21.    public void setMaximum(ValueExpression newValue) { maximum = newValue; }  22.    public void setSize(ValueExpression newValue) { size = newValue; }  23.    public void setValue(ValueExpression newValue) { value = newValue; }  24.    public void setValueChangeListener(MethodExpression newValue)  {  25.       valueChangeListener = newValue;  26.    }  27.  28.    public void setProperties(UIComponent component) {  29.       // always call the superclass method  30.       super.setProperties(component);  31.  32.       component.setValueExpression("size", size);  33.       component.setValueExpression("minimum", minimum);  34.       component.setValueExpression("maximum", maximum);  35.       component.setValueExpression("value", value);  36.       if (valueChangeListener != null)  37.          ((EditableValueHolder) component).addValueChangeListener(  38.                new MethodExpressionValueChangeListener(valueChangeListener));  39.    }  40.  41.    public void release() {  42.       // always call the superclass method  43.       super.release();  44.  45.       minimum = null;  46.       maximum = null;  47.       size = null;  48.       value = null;  49.       valueChangeListener = null;  50.    }  51. }     

Listing 9-14. spinner2/web/WEB-INF/faces-config.xml

  1. <?xml version="1.0"?>   2.   3. <faces-config xmlns="http://java.sun.com/xml/ns/javaee"   4.    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   5.    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee   6.    http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"   7.    version="1.2">   8.   9.    <navigation-rule>  10.       <from-view-id>/index.jsp</from-view-id>  11.       <navigation-case>  12.          <from-outcome>next</from-outcome>  13.          <to-view-id>/next.jsp</to-view-id>  14.       </navigation-case>  15.    </navigation-rule>  16.  17.    <navigation-rule>  18.       <from-view-id>/next.jsp</from-view-id>  19.       <navigation-case>  20.          <from-outcome>again</from-outcome>  21.          <to-view-id>/index.jsp</to-view-id>  22.       </navigation-case>  23.    </navigation-rule>  24.  25.    <managed-bean>  26.       <managed-bean-name>cardExpirationDate</managed-bean-name>  27.       <managed-bean-class>com.corejsf.CreditCardExpiration</managed-bean-class>  28.       <managed-bean-scope>session</managed-bean-scope>  29.    </managed-bean>  30.  31.    <component>  32.       <component-type>com.corejsf.Spinner</component-type>  33.       <component-class>com.corejsf.UISpinner</component-class>  34.    </component>  35.  36.    <render-kit>  37.       <renderer>  38.          <component-family>javax.faces.Input</component-family>  39.          <renderer-type>com.corejsf.Spinner</renderer-type>  40.          <renderer-class>com.corejsf.SpinnerRenderer</renderer-class>  41.       </renderer>  42.    </render-kit>  43.  44.    <application>  45.       <resource-bundle>  46.          <base-name>com.corejsf.messages</base-name>  47.          <var>msgs</var>  48.       </resource-bundle>  49.    </application>  50. </faces-config>     

javax.faces.component.EditableValueHolder

  • void addValueChangeListener(ValueChangeListener listener) JSF 1.2

    Adds a value change listener to this component.

  • void addValidator(Validator val) JSF 1.2

    Adds a validator to this component.


javax.faces.component.ActionSource

  • void addActionListener(ActionListener listener) JSF 1.2

    Adds an action listener to this component.


javax.faces.component.ActionSource2 JSF 1.2

  • void addAction(MethodExpression m)

    Adds an action to this component. The method has return type String and no parameters.


javax.faces.event.MethodExpressionValueChangeListener JSF 1.2

  • MethodExpressionValueChangeListener(MethodExpression m)

    Constructs a value change listener from a method expression. The method must return void and is passed a ValueChangeEvent.


javax.faces.validator.MethodExpressionValidator JSF 1.2

  • MethodExpressionValidator(MethodExpression m)

    Constructs a validator from a method expression. The method must return void and is passed a FacesContext, a UIComponent, and an Object.


javax.faces.event.MethodExpressionActionListener JSF 1.2

  • MethodExpressionActionListener(MethodExpression m)

    Constructs an action listener from a method expression. The method must return void and is passed an ActionEvent.


javax.faces.event.ValueChangeEvent

  • Object getOldValue()

    Returns the component's old value.

  • Object getNewValue()

    Returns the component's new value.


javax.faces.component.ValueHolder

  • Converter getConverter()

    Returns the converter associated with a component. The ValueHolder inter-face is implemented by input and output components.


javax.faces.component.UIComponent

  • ValueExpression getValueExpression(String name) JSF 1.2

    Returns the value expression associated with the given name.


javax.faces.context.FacesContext

  • ELContext getELContext() JSF 1.2

    Returns the expression language context.


javax.el.ValueExpression JSF 1.2

  • Class getType(ELContext context)

    Returns the type of this value expression.


javax.faces.application.Application

  • Converter createConverter(Class targetClass)

    Creates a converter, given its target class. JSF implementations maintain a map of valid converter types, which are typically specified in a faces configuration file. If targetClass is a key in that map, this method creates an instance of the associated converter (specified as the value for the target-Class key) and returns it.

    If targetClass is not in the map, this method searches the map for a key that corresponds to targetClass's interfaces and superclasses, in that order, until it finds a matching class. Once a matching class is found, this method creates an associated converter and returns it. If no converter is found for the targetClass, its interfaces, or its superclasses, this method returns null.



Core JavaServerT Faces
Core JavaServer(TM) Faces (2nd Edition)
ISBN: 0131738860
EAN: 2147483647
Year: 2004
Pages: 84

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net