001    package edu.nrao.sss.webapp.faces.component;
002    
003    import edu.nrao.sss.webapp.faces.conf.SelectorConfiguration;
004    
005    import javax.faces.component.UIInput;
006    import javax.faces.component.UIComponent;
007    import javax.faces.context.FacesContext;
008    import javax.faces.application.FacesMessage;
009    import javax.faces.context.ResponseWriter;
010    import javax.faces.el.ValueBinding;
011    import javax.faces.convert.Converter;
012    import javax.faces.convert.ConverterException;
013    
014    import java.io.IOException;
015    import java.io.InputStreamReader;
016    import java.util.Map;
017    import java.util.List;
018    import java.util.ArrayList;
019    import org.apache.log4j.Logger;
020    
021    /**
022     * Renders a component that allows a user to select a set of rectangles.
023     * <p>These rectangles will be arranged with absolute positioning inside a div
024     * that can be styled with css. A typical usage may be to set the width,
025     * height, and background image of the div and position the rectangles over
026     * pertinent areas of the image. Selecting a rectangle would denote selecting
027     * the object under that rectangle in the image.</p>
028     *
029     * <p>The positions of the rectangles are determined by an xml configuration
030     * file. Currently that file must contain all configurations that this
031     * component will be used for. The file must be at the root of the class path
032     * and be named <code>graphicalSelector.cfg.xml</code>. The simple schema for
033     * these files is below:</p>
034     *
035     * <code><pre>
036     * &lt;?xml version="1.0" encoding="UTF-8" standalone="yes"?&gt;
037     * 
038     * &lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
039     *      &lt;!-- Restricts names to being alphanumeric (with underscores). no spaces, min length 1 --&gt;
040     *      &lt;xs:simpleType name="nameType"&gt;
041     *              &lt;xs:restriction base="xs:string"&gt;
042     *                      &lt;xs:pattern value="[a-zA-Z0-9_]+"/&gt;
043     *              &lt;/xs:restriction&gt;
044     *      &lt;/xs:simpleType&gt;
045     * 
046     *      &lt;xs:complexType name="regionType"&gt;
047     *              &lt;xs:attribute name="name" type="nameType" use="required"/&gt;
048     *              &lt;xs:attribute name="x" type="xs:int" use="required"/&gt;
049     *              &lt;xs:attribute name="y" type="xs:int" use="required"/&gt;
050     *              &lt;xs:attribute name="width" type="xs:int" use="required"/&gt;
051     *              &lt;xs:attribute name="height" type="xs:int" use="required"/&gt;
052     *      &lt;/xs:complexType&gt;
053     * 
054     *      &lt;xs:element name="selector-configuration"&gt;
055     *              &lt;xs:complexType&gt;
056     *                      &lt;xs:sequence&gt;
057     *                              &lt;xs:element name="region-map" minOccurs="1" maxOccurs="unbounded"&gt;
058     *                                      &lt;xs:complexType&gt;
059     *                                              &lt;xs:sequence&gt;
060     *                                                      &lt;xs:element name="region" type="regionType" minOccurs="1" maxOccurs="unbounded"/&gt;
061     *                                              &lt;/xs:sequence&gt;
062     *                                              &lt;xs:attribute name="name" type="nameType" use="required"/&gt;
063     *                                      &lt;/xs:complexType&gt;
064     *                              &lt;/xs:element&gt;
065     *                      &lt;/xs:sequence&gt;
066     *              &lt;/xs:complexType&gt;
067     *      &lt;/xs:element&gt;
068     * &lt;/xs:schema&gt;
069     * </pre></code>
070     */
071    public class GraphicalSelectorUIComponent extends UIInput
072    {
073            private static Logger log = Logger.getLogger(GraphicalSelectorUIComponent.class);
074    
075            private static SelectorConfiguration conf;
076    
077            static 
078            {
079                    try
080                    {
081                            //Find the xml configuration file
082                            InputStreamReader reader = new InputStreamReader(
083                                    GraphicalSelectorUIComponent.class.getResourceAsStream("/graphicalSelector.cfg.xml")
084                            );
085    
086                            //create a configuration from the xml
087                            conf = SelectorConfiguration.fromXml(reader);
088                    }
089    
090                    //if there's an error, log warning, but swallow it and disable the component.
091                    catch (Throwable t)
092                    {
093                            log.warn("Could not load '/graphicalSelector.cfg.xml'. Disabling GraphicalSelectorUIComponent", t);
094                            conf = null;
095                    }
096            }
097    
098            private String mapUsed = null;
099    
100            /**
101             * renders a div containing div's and hidden input fields for each defined
102             * rectangle in the chosen map. If map is null, no rectangles are rendered.
103             * We also render javascript to highlight and select each rectangle.
104             */
105            public void encodeBegin(FacesContext context)
106                    throws IOException
107            {
108                    Map attrs = getAttributes();
109    
110                    String styleClass = (String)attrs.get("styleClass");
111                    String selectedBorderStyle = (String)attrs.get("selectedBorderStyle");
112                    String highlightedBorderStyle = (String)attrs.get("highlightedBorderStyle");
113                    String unhighlightedBorderStyle = (String)attrs.get("unhighlightedBorderStyle");
114    
115                    if (selectedBorderStyle == null)
116                            selectedBorderStyle = "solid 1px red";
117    
118                    if (highlightedBorderStyle == null)
119                            highlightedBorderStyle = "solid 1px yellow";
120    
121                    if (unhighlightedBorderStyle == null)
122                            unhighlightedBorderStyle = "solid 1px black";
123    
124                    List selectedElements = (List)getValue();
125                    if (selectedElements == null)
126                            selectedElements = new ArrayList();
127    
128                    ResponseWriter writer = context.getResponseWriter();
129                    
130                    writer.write("\n");
131    
132                    writer.startElement("div", this); //Start main wrapper div
133    
134                    if (styleClass != null)
135                            writer.writeAttribute("class", styleClass, null);
136                    writer.writeAttribute("style", "position:relative;", null);
137    
138                    writer.write("\n");
139    
140                    boolean misconfigured = false;
141    
142                    //if conf is null, this component is disabled
143                    if (conf == null)
144                    {
145                            misconfigured = true;
146                    }
147    
148                    else
149                    {
150                            SelectorConfiguration.RegionMap map = conf.getRegionMap(getMap());
151    
152                            if (map == null)
153                            {
154                                    misconfigured = true;
155                            }
156    
157                            else
158                            {
159                                    String clientId = getClientId(context);
160    
161                                    //write out the contained rectangles
162                                    for (SelectorConfiguration.Region region : map.getRegions())
163                                    {
164                                            String elementName = region.getName();
165                                            String valueFieldId = clientId + ":" + elementName;
166    
167                                            log.debug("found elementName = " + elementName);
168    
169                                            boolean selected = selectedElements.contains(elementName);
170    
171                                            writer.startElement("div", this);
172    
173                                            writer.writeAttribute("title", elementName, null);
174    
175                                            writer.writeAttribute(
176                                                    "style",
177                                                    getRectangleStyle((selected)? selectedBorderStyle : unhighlightedBorderStyle,
178                                                            region.getWidth(), region.getHeight(), region.getX(), region.getY()),
179                                                    null
180                                            );
181    
182                                            writer.writeAttribute(
183                                                    "onclick",
184                                                    getOnClickJavaScript(valueFieldId, unhighlightedBorderStyle, selectedBorderStyle),
185                                                    null
186                                            );
187    
188                                            writer.writeAttribute(
189                                                    "onmouseout",
190                                                    getOnMouseOutJavaScript(valueFieldId, unhighlightedBorderStyle, selectedBorderStyle),
191                                                    null
192                                            );
193    
194                                            writer.writeAttribute(
195                                                    "onmouseover",
196                                                    getOnMouseOverJavaScript(highlightedBorderStyle),
197                                                    null
198                                            );
199    
200                                            writer.endElement("div");
201                                            writer.write("\n");
202    
203                                            //Write the hidden input field
204                                            writer.startElement("input", this);
205                                            writer.writeAttribute("type", "hidden", null);
206                                            writer.writeAttribute("id", valueFieldId, null);
207                                            writer.writeAttribute("name", valueFieldId, null);
208    
209                                            //Here we need to check the values List to see if this should be
210                                            //initially selected.
211                                            writer.writeAttribute(
212                                                    "value",
213                                                    (selected)? "true" : "false",
214                                                    null
215                                            );
216    
217                                            writer.endElement("input");
218                                            writer.write("\n");
219                                    }
220                            }
221                    }
222    
223                    if (misconfigured)
224                    {
225                            writer.write("This Component is improperly configured!");
226                    }
227    
228                    writer.endElement("div"); //End main wrapper div
229                    writer.write("\n");
230                    writer.write("\n");
231            }
232    
233            private String getRectangleStyle(String unhighlightedBorderStyle, int w, int h, int x, int y)
234            {
235                    StringBuilder style = new StringBuilder("position:absolute;border:");
236                    style.append(unhighlightedBorderStyle);
237                    style.append(";width:");
238                    style.append(w);
239                    style.append("px;height:");
240                    style.append(h);
241                    style.append("px;top:");
242                    style.append(y);
243                    style.append("px;left:");
244                    style.append(x);
245                    style.append("px;");
246    
247                    return style.toString();
248            }
249    
250            private String getOnClickJavaScript(String valueFieldId, String unhighlightedBorderStyle, String selectedBorderStyle)
251            {
252                    StringBuilder js = new StringBuilder("val=document.getElementById('");
253                    js.append(valueFieldId);
254                    //This little bit toggles the value from true to false.
255                    js.append("'); if(val.value=='true'){val.value='false';this.style.border='");
256                    js.append(unhighlightedBorderStyle);
257    
258                    //This one toggles from false to true.
259                    js.append("';} else{val.value='true';this.style.border='");
260                    js.append(selectedBorderStyle);
261                    js.append("';}");
262    
263                    return js.toString();
264            }
265    
266            private String getOnMouseOutJavaScript(String valueFieldId, String unhighlightedBorderStyle, String selectedBorderStyle)
267            {
268                    StringBuilder js = new StringBuilder("this.style.border=(document.getElementById('");
269                    js.append(valueFieldId);
270                    js.append("').value == 'true')? '");
271                    js.append(selectedBorderStyle);
272                    js.append("' : '");
273                    js.append(unhighlightedBorderStyle);
274                    js.append("';");
275    
276                    return js.toString();
277            }
278    
279            private String getOnMouseOverJavaScript(String highlightedBorderStyle)
280            {
281                    return "this.style.border='" + highlightedBorderStyle + "';";
282            }
283    
284            /**Overridden to be empty.*/
285            public void encodeEnd(FacesContext context) throws IOException {}
286            
287            /**Overridden to be empty.*/
288            public void encodeChildren(FacesContext context) throws IOException {}
289    
290            public void decode(FacesContext context)
291            {
292                    boolean misconfigured = false;
293    
294                    //if conf is null, this component is disabled
295                    if (conf == null)
296                    {
297                            misconfigured = true;
298                    }
299    
300                    else
301                    {
302                            SelectorConfiguration.RegionMap map = conf.getRegionMap(getMap());
303    
304                            if (map == null)
305                            {
306                                    misconfigured = true;
307                            }
308    
309                            else
310                            {
311                                    StringBuilder csv = new StringBuilder("");
312    
313                                    List selectedElements = (List)getValue();
314                                    if (selectedElements == null)
315                                            selectedElements = new ArrayList<String>();
316    
317                                    Map parms = context.getExternalContext().getRequestParameterMap();
318                                    String clientId = getClientId(context);
319    
320                                    boolean selectionChanged = false;
321                                    for (SelectorConfiguration.Region region : map.getRegions())
322                                    {
323                                            String elementName = region.getName();
324                                            String valueFieldId = clientId + ":" + elementName;
325    
326                                            String val = (String)parms.get(valueFieldId);
327    
328                                            boolean selected = "true".equals(val);
329    
330                                            //If the element used to be selected, check to see if it still is.
331                                            if (selectedElements.contains(elementName))
332                                            {
333                                                    //selectionChanged: elementName was removed from the selection.
334                                                    if (!selected)
335                                                    {
336                                                            selectionChanged = true;
337                                                    }
338    
339                                                    //No change here. Just add to csv
340                                                    else
341                                                    {
342                                                            csv.append(elementName);
343                                                            csv.append(",");
344                                                    }
345                                            }
346    
347                                            //selectionChanged: elementName was added to the selection
348                                            else if (selected)
349                                            {
350                                                    csv.append(elementName);
351                                                    csv.append(",");
352                                                    selectionChanged = true;
353                                            }
354    
355                                            //else {} element wasn't selected before and it still isn't
356                                    }
357    
358                                    //Only set the submitted value if the selection actually changed.
359                                    if (selectionChanged)
360                                            setSubmittedValue(csv.toString());
361    
362                                    else
363                                            setSubmittedValue(null);
364                            }
365                    }
366    
367                    if (misconfigured)
368                            setSubmittedValue(null);
369            }
370    
371            public String getMap()
372            {
373                    if (this.mapUsed != null)
374                            return this.mapUsed;
375    
376                    ValueBinding vb = getValueBinding("map");
377                    if (vb != null)
378                    {
379                            Object val = vb.getValue(getFacesContext());
380                            
381                            if (val instanceof String && val != null)
382                                    return (String)val;
383                    }
384                    
385                    //else
386                    return null;
387            }
388    
389            public void setMap(String map)
390            {
391                    this.mapUsed = map;
392            }
393            
394            public Object saveState(FacesContext context)
395            {
396                    Object[] state = new Object[2];
397                    state[0] = super.saveState(context);
398                    state[1] = this.mapUsed;
399                    
400                    return state;
401            }
402            
403            public void restoreState(FacesContext context, Object state)
404            {
405                    Object[] myState = (Object[])state;
406                    super.restoreState(context, myState[0]);
407                    setMap((String)myState[1]);
408            }
409    
410            /**
411             * Overridden to provide an appropriate CSV to List converter for our value.
412             */
413            public Converter getConverter()
414            {
415                    return csvToList;
416            }
417    
418            private static Converter csvToList = new CsvConverter();
419    
420            /**
421             * Converts from a comma separated value String to a List of Strings and
422             * back. Will never return a null List in getAsObject.
423             */
424            protected static class CsvConverter implements Converter
425            {
426                    public Object getAsObject(FacesContext context, UIComponent component, String value)
427                    {
428                            List<String> list = new ArrayList<String>();
429                            if (value != null && value.length() > 0)
430                            {
431                                    for (String s : value.split(","))
432                                    {
433                                            s = s.trim();
434                                            if (s.length() > 0)
435                                                    list.add(s);
436                                    }
437                            }
438    
439                            return list;
440                    }
441    
442                    public String getAsString(FacesContext context, UIComponent component, Object value)
443                    {
444                            if (value == null)
445                                    return "";
446    
447                            else if (value instanceof List)
448                            {
449                                    StringBuilder csv = new StringBuilder();
450                                    for (Object v : (List)value)
451                                    {
452                                            csv.append(v.toString().trim());
453                                            csv.append(",");
454                                    }
455    
456                                    return csv.toString();
457                            }
458    
459                            else
460                            {
461                                    throw new ConverterException(new FacesMessage("Can not Convert objects of type: " + value.getClass()));
462                            }
463                    }
464            }
465    }