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 * <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 037 * 038 * <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> 039 * <!-- Restricts names to being alphanumeric (with underscores). no spaces, min length 1 --> 040 * <xs:simpleType name="nameType"> 041 * <xs:restriction base="xs:string"> 042 * <xs:pattern value="[a-zA-Z0-9_]+"/> 043 * </xs:restriction> 044 * </xs:simpleType> 045 * 046 * <xs:complexType name="regionType"> 047 * <xs:attribute name="name" type="nameType" use="required"/> 048 * <xs:attribute name="x" type="xs:int" use="required"/> 049 * <xs:attribute name="y" type="xs:int" use="required"/> 050 * <xs:attribute name="width" type="xs:int" use="required"/> 051 * <xs:attribute name="height" type="xs:int" use="required"/> 052 * </xs:complexType> 053 * 054 * <xs:element name="selector-configuration"> 055 * <xs:complexType> 056 * <xs:sequence> 057 * <xs:element name="region-map" minOccurs="1" maxOccurs="unbounded"> 058 * <xs:complexType> 059 * <xs:sequence> 060 * <xs:element name="region" type="regionType" minOccurs="1" maxOccurs="unbounded"/> 061 * </xs:sequence> 062 * <xs:attribute name="name" type="nameType" use="required"/> 063 * </xs:complexType> 064 * </xs:element> 065 * </xs:sequence> 066 * </xs:complexType> 067 * </xs:element> 068 * </xs:schema> 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 }