001    package edu.nrao.sss.webapp.faces.component;
002    
003    import javax.faces.component.UIInput;
004    import javax.faces.context.FacesContext;
005    import javax.faces.context.ResponseWriter;
006    import javax.faces.convert.ConverterException;
007    import javax.faces.el.ValueBinding;
008    
009    import java.io.IOException;
010    import java.util.Map;
011    import java.util.List;
012    import java.util.ArrayList;
013    import java.util.Hashtable;
014    import java.util.Collections;
015    
016    import org.apache.log4j.Logger;
017    
018    import edu.nrao.sss.webapp.faces.renderer.RendererUtils;
019    
020    public class NotesUIComponent extends UIInput
021    {
022            private static final Logger log = Logger.getLogger(NotesUIComponent.class);
023    
024            protected static final String RENDERED_SCRIPT_KEY =
025                    "edu.nrao.sss.webapp.faces.component.NotesUIComponent.JavaScriptRendered";
026    
027            private Boolean enabled = null;
028            private Hashtable<String, Boolean> collapsedState = new Hashtable<String, Boolean>();
029    
030            public void encodeBegin(FacesContext context)
031                    throws IOException
032            {
033                    if (!isRendered())
034                            return;
035    
036                    List notes = (List)getValue();
037                    ResponseWriter writer = context.getResponseWriter();
038    
039                    String id = getClientId(context);
040                    Map attrs = getAttributes();
041                    String styleClass = (String)attrs.get("styleClass");
042    
043                    RendererUtils.writeLineSeparator(writer);
044                    writer.startElement("div", this);
045                    writer.writeAttribute("id", id, null);
046                    writer.writeAttribute("class", styleClass, null);
047    
048                    if (notes != null && notes.size() > 0)
049                    {
050                            int size = notes.size();
051    
052                            if (isEnabled())
053                            {
054                                    //render new note link
055                                    writer.write('\n');
056                                    encodeNewLink(writer, id);
057                            }
058    
059                            for (int i = 0; i < size; i++)
060                            {
061                                    Object n = notes.get(i);
062                                    writer.write('\n');
063                                    encodeNote(context, id, n.toString(), i);
064                            }
065                    }
066    
067                    else
068                    {
069                            writer.write('\n');
070                            writer.startElement("em", this);
071                            writer.write("There are currently no notes available.");
072                            writer.endElement("em");
073                            writer.write('\n');
074    
075                            if (isEnabled())
076                            {
077                                    //render new note link
078                                    writer.write('\n');
079                                    encodeNewLink(writer, id);
080                            }
081                    }
082    
083                    writer.write('\n');
084                    writer.endElement("div");
085                    writer.write('\n');
086            }
087    
088            /**
089             * Encodes <code>note</code> rendering the note, a collapse/expand button,
090             * and a remove link (depending on the isEnabled flag).
091             */
092            protected void encodeNote(FacesContext context, String id, String note, int index)
093                    throws IOException
094            {
095                    String nid = getNoteId(context, index);
096                    String hid = getNoteHeaderId(nid);
097                    String sumid = getNoteSummaryId(nid);
098    
099                    if (note == null)
100                    {
101                            note = "";
102                    }
103    
104                    ResponseWriter writer = context.getResponseWriter();
105                    Map attrs = getAttributes();
106                    String headerClass = (String)attrs.get("headerClass");
107    
108                    writer.startElement("span", this);
109                    writer.writeAttribute("id", hid, null);
110                    writer.writeAttribute("class", headerClass, null);
111                    writer.write("Note #" + (index + 1));
112                    writer.write("\n");
113    
114                    if (isEnabled())
115                    {
116                            //Remove link that pops up a warning dialog...
117                            encodeRemoveLink(writer, id, nid, hid);
118                            writer.write('\n');
119                    }
120    
121                    //If collapsed, show summary
122                    String summary = note;
123                    if (note.length() > 30)
124                    {                         
125                            summary = note.substring(0, 27) + "...";
126                    }
127    
128                    writer.startElement("span", this);
129                    writer.writeAttribute("id", sumid, null);
130                    writer.writeAttribute("style", (isCollapsed(nid))? "" : "display:none;", null);
131                    writer.write(summary);
132                    writer.endElement("span");
133                    writer.write('\n');
134    
135                    //collapse button w/ styleClass
136                    encodeCollapseButton(writer, nid, sumid);
137                    writer.write('\n');
138    
139                    writer.endElement("span");
140                    writer.write('\n');
141    
142                    if (isEnabled())
143                    {
144                            //textarea w/styleClass
145                            writer.startElement("textarea", this);
146                            writer.writeAttribute("class", "xxx", null);
147                            writer.writeAttribute("id", nid, null);
148                            writer.writeAttribute("name", nid, null);
149                            writer.writeAttribute("style", (isCollapsed(nid))? "display:none;" : "", null);
150                            writer.write('\n');
151                            writer.write(note.toString());
152                            writer.endElement("textarea");
153                    }
154    
155                    else
156                    {
157                            //span with note if not enabled.
158                            writer.startElement("div", this);
159                            writer.writeAttribute("class", "xxx", null);
160                            writer.writeAttribute("id", nid, null);
161                            writer.writeAttribute("style", (isCollapsed(nid))? "display:none;" : "", null);
162                            String formattedNote = note.replaceAll("\n", "<p/>");
163                            writer.write('\n');
164                            writer.write(formattedNote);
165                            writer.endElement("div");
166                    }
167            }
168    
169            /**
170             * Writes a link that will create a new note widget (via JS).
171             * @param id The client id of this component
172             */
173            protected void encodeNewLink(ResponseWriter writer, String id)
174                    throws IOException
175            {
176                    writer.startElement("a", this);
177                    writer.writeAttribute("href", "#", null);
178                    writer.writeAttribute("onclick",
179                            "return NotesUIComponent_NewNote('" + id + "');",
180                            null
181                    );
182                    writer.write("(New Note)");
183                    writer.endElement("a");
184                    writer.write('\n');
185            }
186    
187            protected void encodeRemoveLink(ResponseWriter writer, String pid, String nid, String hid)
188                    throws IOException
189            {
190                    String linkStyleClass = (String)getAttributes().get("linkStyleClass");
191                    String dialogStyleClass = (String)getAttributes().get("dialogStyleClass");
192                    String removeLinkClass = (String)getAttributes().get("removeLinkClass");
193                    
194                    String dialogId = nid + "_popup";
195                    
196                    writer.write('\n');
197                    writer.write('\n');
198    
199                    //Start a wrapper div with position: relative so the popup can be
200                    //positioned relative to it.
201                    writer.startElement("div", this);
202    
203                    if (removeLinkClass != null)
204                            writer.writeAttribute("class", removeLinkClass, null);
205    
206                    writer.write('\n');
207    
208                    //This is our link section.
209                    writer.startElement("a", this);
210                    writer.writeAttribute("href", "#", null);
211    
212                    if (linkStyleClass != null)
213                            writer.writeAttribute("class", linkStyleClass, null);
214    
215                    writer.writeAttribute("onclick",
216                            "return NotesUIComponent_OpenRemoveNoteWarning('" + dialogId + "');",
217                            null
218                    );
219    
220                    writer.write("(Remove Note)");
221    
222                    writer.endElement("a");
223                    writer.write('\n');
224    
225                    //This is our popupDialog
226                    writer.startElement("div", this);
227                    writer.writeAttribute("id", dialogId, null);
228                    writer.writeAttribute("style", "display:none;", null);
229    
230                    if (dialogStyleClass != null)
231                            writer.writeAttribute("class", dialogStyleClass, null);
232    
233                    writer.write('\n');
234    
235                    //XXX render dialog content here
236                    writer.startElement("em", this);
237                    writer.write("Are you sure you want to remove this note?");
238                    writer.endElement("em");
239    
240                    writer.startElement("div", this);
241    
242                    writer.startElement("input", this);
243                    writer.writeAttribute("type", "button", null);
244    
245                    StringBuilder sb = new StringBuilder("return NotesUIComponent_RemoveNote('");
246                    sb.append(pid);
247                    sb.append("', '");
248                    sb.append(nid);
249                    sb.append("', '");
250                    sb.append(hid);
251                    sb.append("');");
252                    writer.writeAttribute("onclick", sb.toString(), null);
253    
254                    writer.writeAttribute("value", "Remove", null);
255                    writer.endElement("input");
256    
257                    writer.startElement("input", this);
258                    writer.writeAttribute("type", "button", null);
259                    writer.writeAttribute("onclick",
260                            "return NotesUIComponent_CloseRemoveNoteWarning('" + dialogId + "');",
261                            null
262                    );
263    
264                    writer.writeAttribute("value", "Cancel", null);
265                    writer.endElement("input");
266    
267                    writer.endElement("div");
268    
269                    writer.write('\n');
270    
271                    //End the hidden popup
272                    writer.endElement("div");
273                    writer.write('\n');
274    
275                    //End the wrapper;
276                    writer.endElement("div");
277                    writer.write('\n');
278                    writer.write('\n');
279            }
280    
281            /**
282             * encodes a link that calls javascript functions to expand and collapse a
283             * note. Also encodes a hidden field to keep track of the expanded state of
284             * the note.
285             */
286            protected void encodeCollapseButton(ResponseWriter writer, String nid, String sumid)
287                    throws IOException
288            {
289                    //Render hidden field with known id so that we know the collapsed state of
290                    //each note.
291                    String sid = getNoteStateId(nid);
292    
293                    writer.write('\n');
294                    writer.startElement("input", this);
295                    writer.writeAttribute("type", "hidden", null);
296                    writer.writeAttribute("id", sid, null);
297                    writer.writeAttribute("name", sid, null);
298    
299                    writer.writeAttribute("value", (isCollapsed(nid))? "true" : "false", null);
300                    writer.endElement("input");
301                    writer.write('\n');
302    
303    
304                    Map attrs = getAttributes();
305                    String collapseLinkClass = (String)attrs.get("collapseLinkClass");
306    
307                    //Write out the link
308                    writer.startElement("a", this);
309                    writer.writeAttribute("href", "#", null);
310                    writer.writeAttribute("class", collapseLinkClass, null);
311    
312                    StringBuilder onclick = new StringBuilder("return NotesUIComponent_ToggleCollapse('");
313                    onclick.append(nid);
314                    onclick.append("', '");
315                    onclick.append(sid);
316                    onclick.append("', '");
317                    onclick.append(sumid);
318                    onclick.append("');");
319    
320                    writer.writeAttribute("onclick", onclick.toString(), null);
321                    writer.endElement("a");
322                    writer.write('\n');
323            }
324    
325            /**
326             * Overridden to output the necessary javascript for this component. If the
327             * javascript has already be rendered for this response, nothing is done.
328             */
329            @SuppressWarnings("unchecked")
330            public void encodeEnd(FacesContext context)
331                    throws IOException
332            {
333                    if (!isRendered())
334                            return;
335    
336                    List notes = (List)getValue();
337    
338                    Map requestMap = context.getExternalContext().getRequestMap();
339                    Boolean scriptRendered = (Boolean)requestMap.get(RENDERED_SCRIPT_KEY);
340    
341                    if (scriptRendered != Boolean.TRUE)
342                    {
343                            requestMap.put(RENDERED_SCRIPT_KEY, Boolean.TRUE);
344                            ResponseWriter writer = context.getResponseWriter();
345    
346                            writer.startElement("script", this);
347                            writer.writeAttribute("type", "text/javascript", null);
348                            encodeNewNoteJSFunction(writer, (notes == null)? 0 : notes.size());
349                            encodeRemoveNoteJSFunctions(writer);
350                            encodeCollapseNoteJSFunctions(writer);
351                            writer.endElement("script");
352                    }
353            }
354    
355            /**
356             * writes a javascript function that inserts a new notes widget into the html
357             * at the end of the list of notes widgets.
358             */
359            protected void encodeNewNoteJSFunction(ResponseWriter writer, int numNotes)
360                    throws IOException
361            {
362                    Map attrs = getAttributes();
363                    String headerClass = (String)attrs.get("headerClass");
364                    String collapseLinkClass = (String)attrs.get("collapseLinkClass");
365    
366                    StringBuilder sb = new StringBuilder("\nvar NotesUIComponent_NewNoteIndex = ");
367                    sb.append(numNotes);
368                    sb.append(";\n");
369                    sb.append("function NotesUIComponent_NewNote(pid) {\n");
370                    sb.append("\tnid = pid + ':' + NotesUIComponent_NewNoteIndex;\n");
371                    sb.append("\thid = nid + '_header';\n");
372                    sb.append("\tsid = nid + '_state';\n");
373                    sb.append("\tdialogId = nid + '_popup';\n");
374                    sb.append("\tsumid = nid + '_summary';\n\n");
375    
376                    //Now we Start the code that actually adds components to the DOM and whatnot.
377                    sb.append("\tparent = document.getElementById(pid);\n");
378                    sb.append("\tnoteHeader = document.createElement('span');\n");
379                    sb.append("\tnoteHeader.id = hid;\n");
380                    sb.append("\tnoteHeader.className = '" + headerClass + "';\n");
381                    sb.append("\tnoteHeader.innerHTML = 'Note #' + (NotesUIComponent_NewNoteIndex + 1);\n");
382    
383                    //Begin wrapper for floating dialog (for the remove link warning)
384                    sb.append("\n\tdialogWrapper = document.createElement('div');\n");
385                    sb.append("\tdialogWrapper.className = 'xxx'\n");
386                    sb.append("\tnoteHeader.appendChild(dialogWrapper);\n");
387                    
388                    //Now start on contents of wrapper
389                    sb.append("\tdialogTrigger = document.createElement('a');\n");
390                    sb.append("\tdialogTrigger.href = '#';\n");
391                    sb.append("\tdialogTrigger.className = 'xxx';\n");
392                    sb.append("\tdialogTrigger.onclick = function(){return NotesUIComponent_OpenRemoveNoteWarning(dialogId);};\n");
393                    sb.append("\tdialogTrigger.innerHTML = '(Remove Note)';\n");
394                    sb.append("\tdialogWrapper.appendChild(dialogTrigger);\n");
395    
396                    //Add the popup div...
397                    sb.append("\tpopup = document.createElement('div');\n");
398                    sb.append("\tpopup.id = dialogId;\n");
399                    sb.append("\tpopup.className = 'xxx';\n");
400                    sb.append("\tpopup.style.display = 'none';\n");
401                    sb.append("\tdialogWrapper.appendChild(popup);\n");
402    
403                    //Contents of popup
404                    sb.append("\tquestion = document.createElement('em');\n");
405                    sb.append("\tquestion.innerHTML = 'Are you sure you want to remove this note?';\n");
406                    sb.append("\tpopup.appendChild(question);\n");
407    
408                    sb.append("\tbuttonBar = document.createElement('div');\n");
409                    sb.append("\tpopup.appendChild(buttonBar);\n");
410                    sb.append("\tconfirm = document.createElement('input');\n");
411                    sb.append("\tconfirm.type = 'button';\n");
412                    sb.append("\tconfirm.value = 'Remove';\n");
413                    sb.append("\tconfirm.onclick = function(){return NotesUIComponent_RemoveNote(pid, nid, hid);};\n");
414                    sb.append("\tbuttonBar.appendChild(confirm);\n");
415    
416                    //cancel button
417                    sb.append("\tcancel = document.createElement('input');\n");
418                    sb.append("\tcancel.type = 'button';\n");
419                    sb.append("\tcancel.value = 'Cancel';\n");
420                    sb.append("\tcancel.onclick = function(){return NotesUIComponent_CloseRemoveNoteWarning(dialogId);};\n");
421                    sb.append("\tbuttonBar.appendChild(cancel);\n");
422    
423                    //Done with popup now! Moving on to the note summary
424                    sb.append("\tsummary = document.createElement('span');\n");
425                    sb.append("\tsummary.id = sumid;\n");
426                    sb.append("\tsummary.style.display = 'none';\n");
427                    sb.append("\tsummary.innerHTML = 'Enter new note text here.';\n");
428                    sb.append("\tnoteHeader.appendChild(summary);\n");
429    
430                    //Now the collapse button foo.
431                    sb.append("\tcollapsedState = document.createElement('input');\n");
432                    sb.append("\tcollapsedState.type = 'hidden';\n");
433                    sb.append("\tcollapsedState.id = sid;\n");
434                    sb.append("\tcollapsedState.name = sid;\n");
435                    sb.append("\tcollapsedState.value = false;\n");
436                    sb.append("\tnoteHeader.appendChild(collapsedState);\n");
437    
438                    sb.append("\tcollapseLink = document.createElement('a');\n");
439                    sb.append("\tcollapseLink.href = '#';\n");
440                    sb.append("\tcollapseLink.className = '" + collapseLinkClass + "';\n");
441                    sb.append("\tcollapseLink.onclick = function (){ return NotesUIComponent_ToggleCollapse(nid, sid, sumid); };\n");
442                    sb.append("\tnoteHeader.appendChild(collapseLink);\n");
443    
444                    //Deep breath...almost done. Now we can render the actual textarea
445                    sb.append("\tta = document.createElement('textarea');\n");
446                    sb.append("\tta.id = nid;\n");
447                    sb.append("\tta.name = nid;\n");
448                    sb.append("\tta.className = 'xxx';\n");
449                    sb.append("\tta.innerHTML = 'Enter new note text here.';\n");
450    
451                    //Now connect our 2 new high level elements (header and textarea) to the actual document.
452                    sb.append("\tparent.appendChild(noteHeader);\n");
453                    sb.append("\tparent.appendChild(ta);\n");
454    
455    
456                    //don't forget to increment our NewNoteIndex so it works again next time.
457                    sb.append("\n\tNotesUIComponent_NewNoteIndex = NotesUIComponent_NewNoteIndex + 1;\n");
458                    sb.append("\treturn false;\n");
459                    sb.append("}\n\n");
460    
461                    writer.write(sb.toString());
462            }
463    
464            /**
465             * writes javascript functions that handle removing a note and poping up a
466             * dialog warning them with confirm and cancel options.
467             */
468            protected void encodeRemoveNoteJSFunctions(ResponseWriter writer)
469                    throws IOException
470            {
471                    StringBuilder sb = new StringBuilder("function NotesUIComponent_OpenRemoveNoteWarning(dialogId) {\n");
472                    sb.append("\tvar elementStyle = document.getElementById(dialogId).style;\n");
473                    sb.append("\telementStyle.display='block';\n");
474                    sb.append("\treturn false;\n");
475                    sb.append("}\n\n");
476    
477                    sb.append("function NotesUIComponent_CloseRemoveNoteWarning(dialogId) {\n");
478                    sb.append("\tvar elementStyle = document.getElementById(dialogId).style;\n");
479                    sb.append("\telementStyle.display='none';\n");
480                    sb.append("\treturn false;\n");
481                    sb.append("}\n\n");
482    
483                    sb.append("function NotesUIComponent_RemoveNote(pid, nid, hid) {\n");
484                    sb.append("\tvar parent = document.getElementById(pid);\n");
485                    sb.append("\tvar note = document.getElementById(nid);\n");
486                    sb.append("\tvar header = document.getElementById(hid);\n");
487                    sb.append("\tparent.removeChild(header);\n");
488                    sb.append("\tparent.removeChild(note);\n");
489                    sb.append("\treturn false;\n");
490                    sb.append("}\n\n");
491    
492                    writer.write(sb.toString());
493            }
494    
495            /**
496             * writes a javascript function that toggles the collapsed state of a note.
497             * Also outputs hidden form fields to keep track of said state.
498             */
499            protected void encodeCollapseNoteJSFunctions(ResponseWriter writer)
500                    throws IOException
501            {
502                    StringBuilder sb = new StringBuilder("function NotesUIComponent_ToggleCollapse(nid, sid, sumid) {\n");
503                    sb.append("\tcollapsed = document.getElementById(sid);\n");
504                    sb.append("\tnote = document.getElementById(nid);\n");
505                    sb.append("\tsummary = document.getElementById(sumid);\n");
506                    sb.append("\tif (collapsed.value == 'true') {\n");
507                    sb.append("\t\tcollapsed.value = false;\n");
508                    sb.append("\t\tnote.style.display = 'block';\n");
509                    sb.append("\t\tsummary.style.display = 'none';\n");
510                    sb.append("\t}\n");
511                    sb.append("\telse {\n");
512                    sb.append("\t\tcollapsed.value = true;\n");
513                    sb.append("\t\tnote.style.display = 'none';\n");
514                    sb.append("\t\tsummary.style.display = 'inline';\n");
515                    sb.append("\t}\n");
516                    sb.append("\treturn false;\n");
517                    sb.append("}\n\n");
518    
519                    writer.write(sb.toString());
520            }
521    
522            public void decode(FacesContext context)
523            {
524                    String id = getClientId(context);
525    
526                    Map parms = context.getExternalContext().getRequestParameterMap();
527    
528                    List<String> notes = new ArrayList<String>();
529                    List<String> noteIds = new ArrayList<String>();
530    
531                    Hashtable<String, Boolean> state = new Hashtable<String, Boolean>();
532                    for (Object key : parms.keySet())
533                    {
534                            String sKey = (String)key;
535                            //XXX probably want to compile a pattern here to speed things up.
536                            if (sKey.matches("^" + id + ":\\d+$"))
537                            {
538                                    noteIds.add(sKey);
539                                    String sid = getNoteStateId(sKey);
540                                    String noteState = (String)parms.get(sid);
541                                    noteState = (noteState == null)? "true" : noteState;
542                                    state.put(sKey, Boolean.valueOf(noteState));
543                            }
544                    }
545    
546                    setCollapsed(state);
547    
548                    Collections.sort(noteIds);
549    
550                    for (String key : noteIds)
551                    {
552                            notes.add((String)parms.get(key));
553                    }
554    
555                    setSubmittedValue(notes);
556            }
557    
558            /**
559             * Override the getConvertedValue method to return the List? Or just return * the thing it gets unchanged?
560             */
561            protected Object getConvertedValue(FacesContext context, Object newSubmittedValue)
562                    throws ConverterException
563            {
564                    return newSubmittedValue;
565            }
566    
567            /**
568             * Overridden to compare 2 lists.
569             * @return true if the new value is different from the old value.
570             */
571            protected boolean compareValues(Object previous, Object value)
572            {
573                    log.debug("compareValues called");
574    
575                    if (previous == null)
576                            return value != null;
577    
578                    if (value == null)
579                            return previous != null;
580    
581                    log.debug("compareValues: both non-null.");
582                    if (previous == value)
583                            return false;
584    
585                    List lP = (List)previous;
586                    List lV = (List)value;
587    
588                    if (lP.size() != lV.size())
589                            return true;
590    
591                    log.debug("compareValues: both same size.");
592                    for (int i = 0, size = lP.size(); i < size; i++)
593                    {
594                            if (!lP.get(i).equals(lV.get(i)))
595                                    return true;
596                    }
597    
598                    log.debug("compareValues: identical lists.");
599                    return false;
600            }
601    
602            public Object saveState(FacesContext context)
603            {
604                    Object[] state = new Object[3];
605                    state[0] = super.saveState(context);
606                    state[1] = this.enabled;
607                    state[2] = this.collapsedState;
608                    
609                    return state;
610            }
611            
612            @SuppressWarnings("unchecked")
613            public void restoreState(FacesContext context, Object state)
614            {
615                    Object[] myState = (Object[])state;
616                    super.restoreState(context, myState[0]);
617                    setEnabled((Boolean)myState[1]);
618                    setCollapsed((Hashtable)myState[2]);
619            }
620            
621            public boolean isEnabled()
622            {
623                    if (this.enabled != null)
624                            return this.enabled;
625                    
626                    ValueBinding vb = getValueBinding("enabled");
627                    if (vb != null)
628                    {
629                            Object val = vb.getValue(getFacesContext());
630                            
631                            if (val instanceof Boolean && val != null)
632                                    return (Boolean)val;
633                    }
634                    
635                    //else
636                    return Boolean.TRUE;
637            }
638            
639            public void setEnabled(Boolean e)
640            {
641                    this.enabled = e;
642            }
643    
644            public Boolean isCollapsed(String nid)
645            {
646                    Boolean c = this.collapsedState.get(nid);
647                    if (c == null)
648                            c = Boolean.TRUE;
649    
650                    return c;
651            }
652    
653            public void setCollapsed(Hashtable<String, Boolean> state)
654            {
655                    this.collapsedState = state;
656            }
657    
658            protected String getNoteId(FacesContext context, int noteIndex)
659            {
660                    return getClientId(context) + ":" + noteIndex;
661            }
662            
663            protected String getNoteStateId(String nid)
664            {
665                    return nid + "_state";
666            }
667            
668            protected String getNoteHeaderId(String nid)
669            {
670                    return nid + "_header";
671            }
672            
673            protected String getNoteSummaryId(String nid)
674            {
675                    return nid + "_summary";
676            }
677            
678            /** Overridden to be empty. */
679            public void encodeChildren(FacesContext context) throws IOException {}
680    }