001    package edu.nrao.sss.html;
002    
003    import java.io.IOException;
004    import java.io.Writer;
005    import java.util.ArrayList;
006    import java.util.Collection;
007    import java.util.HashMap;
008    import java.util.Map;
009    
010    import javax.swing.text.html.HTML;
011    
012    /**
013     * Parent to other HTML elements.
014     * <p>
015     * <b>Version Info:</b>
016     * <table style="margin-left:2em">
017     *   <tr><td>$Revision: 501 $</td></tr>
018     *   <tr><td>$Date: 2007-04-04 09:13:28 -0600 (Wed, 04 Apr 2007) $</td></tr>
019     *   <tr><td>$Author: dharland $</td></tr>
020     * </table></p>
021     * 
022     * @author David M. Harland
023     * @since 2007-03-16
024     */
025    public abstract class HtmlElement
026    {
027      /** HTML representation of a non-breaking space. */
028      public static final String NBSP_HTML = "&nbsp;";
029      
030      /**
031       * The unicode text into which java's HTML parsers convert
032       * {@link #NBSP_HTML}.  When writing HTML elements to
033       * HTML text, this is the value used for a non-breaking space.
034       * This representation is ASCII 0xA0, or unicode 00A0.
035       */
036      public static final String NBSP_UNICODE = "\u00A0";
037      
038      /**
039       * Plain text representation of {@link #NBSP_HTML}.  When writing HTML
040       * elements to plain (non-HTML) text, this is the value used for
041       * a non-breaking space.  This representation is the normal space
042       * character, ASCII 32 (0x20).
043       */
044      public static final String NBSP_TEXT = " ";
045      
046      HTML.Tag tag;  //Not directly publicly mutable; can be reset by subclasses
047    
048      Map<HTML.Attribute, HtmlAttribute> attributes; //Not directly pub'ly mutable
049      
050      /**
051       * Helps create a new instance.
052       * 
053       * @param htmlTag the kind of element this is.
054       */
055      HtmlElement(HTML.Tag htmlTag)
056      {
057        if (htmlTag == null) throw
058          new IllegalArgumentException("May not send null tag to constructor.");
059        
060        tag        = htmlTag;
061        attributes = new HashMap<HTML.Attribute, HtmlAttribute>();
062      }
063      
064      /**
065       * Returns the HTML tag for this element.
066       * @return the HTML tag for this element.
067       */
068      public HTML.Tag getTag()
069      {
070        return tag;
071      }
072      
073      /**
074       * Returns <i>true</i> if this is a simple element.
075       * A simple element is one that may not have content, such as the line break
076       * (&lt;br/&gt;) element.
077       * 
078       * @return <i>true</i> if this is a simple element.
079       */
080      public abstract boolean isSimple();
081      
082      //============================================================================
083      // ATTRIBUTES
084      //============================================================================
085      
086      /**
087       * Adds {@code newAttribute} to this element.  If this element currently holds
088       * an attribute of the same type as {@code newAttribute}, it is replaced by
089       * {@code newAttribute}.
090       * 
091       * @param newAttribute a new attribute for this element.
092       *                     If this value is <i>null</i> this method does nothing.
093       *                     
094       * @return an attribute of the same type as {@code newAttribute}, or
095       *         <i>null</i> if this element had no attribute of that type.
096       */
097      public HtmlAttribute addAttribute(HtmlAttribute newAttribute)
098      {
099        return (newAttribute == null) ? null
100                                      : attributes.put(newAttribute.getType(),
101                                                       newAttribute);
102      }
103      
104      /**
105       * Removes from this element its attribute of the given type.
106       * 
107       * @param unwantedType the type of attribute to remove from this element.
108       * 
109       * @return the attribute of the given type held by this element, or
110       *         <i>null</i> if it held no such attribute.
111       */
112      public HtmlAttribute removeAttribute(HTML.Attribute unwantedType)
113      {
114        return (unwantedType == null) ? null : attributes.remove(unwantedType);
115      }
116      
117      /**
118       * Removes all attributes from this element.
119       * @return the number of attributes of this element prior to their removal.
120       */
121      public int removeAllAttributes()
122      {
123        int oldCount = attributes.size();
124        
125        if (oldCount > 0)
126          attributes.clear();
127        
128        return oldCount;
129      }
130      
131      /**
132       * Returns the attribute of the given type held by this element, if any.
133       * If this element holds no such element, <i>null</i> is returned.
134       * 
135       * @param attributeType the type of attribute desired.
136       * 
137       * @return the attribute of the given type held by this element, if any.
138       *         If this element holds no such element, <i>null</i> is returned.
139       */
140      public HtmlAttribute getAttribute(HTML.Attribute attributeType)
141      {
142        return (attributeType == null) ? null : attributes.get(attributeType);
143      }
144      
145      /**
146       * Returns the attributes of this element.
147       * <p>
148       * The returned collection is not held by this element, so
149       * any changes made to it will be <i>not</i> reflected in this object.
150       * The attributes in the collection, however, are those held by this
151       * element.</p>
152       * 
153       * @return the attributes of this element.
154       *         The collection is guaranteed to be non-null, but could be empty.
155       */
156      public Collection<HtmlAttribute> getAttributes()
157      {
158        return new ArrayList<HtmlAttribute>(attributes.values());
159      }
160      
161      /**
162       * Returns the value of the given attribute, or the empty string
163       * (<tt>""</tt>) if this element has no such attribute.
164       * 
165       * @param attributeType the name of an attribute of this element
166       *                      for which a value is sought.
167       *                      
168       * @return the value of the given attribute.
169       */
170      public String getAttributeValue(HTML.Attribute attributeType)
171      {
172        HtmlAttribute attribute = attributes.get(attributeType);
173        
174        return attribute == null ? "" : attribute.getValue();
175      }
176      
177      /**
178       * Sets the attributes of this element based on the given text.
179       * <p>
180       * The expected form of {@code attributeText} is a series of
181       * <tt>name="value"</tt> pairs, where each pair in the series is
182       * separated by whitespace.  It is expected that the opening and
183       * closing tag markers, along with the tag name itself, are
184       * <i>not</i> present in the text.  Leading and trailing whitespace
185       * is permitted and will be stripped during parsing.  The <tt>value</tt>
186       * of each pair should be in either single (') or double (") quotes.</p>
187       * <p>
188       * If anything goes wrong during parsing, an {@code IllegalArgumentException}
189       * is thrown.  All successfully parsed pairs up to the point of the exception
190       * will be maintained by this instance.</p>
191       * 
192       * @param attributeText a series of <tt>name="value"</tt> pairs, where
193       *                      each pair in the series is separated by whitespace.
194       */
195      void parseAttributes(String attributeText)
196      {
197        int equalsPosition = attributeText.indexOf('=');
198        
199        while (equalsPosition > -1)
200        {
201          //Will let HtmlAttribute.parse worry about situation of no quotes.
202          //Here we just try to determine which kind comes first.
203          int singleQuotePos = attributeText.indexOf('\'');
204          int doubleQuotePos = attributeText.indexOf('"');
205          int startQuotePos;
206          
207          char quoteChar;
208          
209          if ((singleQuotePos > -1) && (singleQuotePos < doubleQuotePos))
210          {
211            quoteChar = '\'';
212            startQuotePos = singleQuotePos;
213          }
214          else
215          {
216            quoteChar = '"';
217            startQuotePos = doubleQuotePos;
218          }
219          
220          //Send just one name="value" pair to parse method
221          int    endQuotePos = attributeText.indexOf(quoteChar, startQuotePos+1);
222          String parseText   = attributeText.substring(0, endQuotePos+1);
223          
224          HtmlAttribute attribute = HtmlAttribute.parse(parseText);
225          attributes.put(attribute.getType(), attribute);
226          
227          //Trim text to part not yet parsed
228          attributeText = attributeText.substring(parseText.length());
229          
230          equalsPosition = attributeText.indexOf('=');
231        }
232      }
233      
234      /**
235       * Copies the attributes of the other element into this one.
236       * @param other a provider of HTML attributes.
237       */
238      protected void copyAttributesOf(HtmlElement other)
239      {
240        for (HtmlAttribute oAttr : other.getAttributes())
241          addAttribute(new HtmlAttribute(oAttr.getType(), oAttr.getValue()));
242      }
243      
244      //============================================================================
245      // WRITING
246      //============================================================================
247      
248      /**
249       * Writes this element to the given device.
250       * 
251       * @param device the destination of the HTML.
252       * 
253       * @param padding the number of spaces to write before writing this element.
254       * 
255       * @param newLine if this value is <i>true</i>, a new-line character is
256       *                written after this element.
257       *                
258       * @throws IOException if anything goes wrong while writing.
259       */
260      public void writeHtmlTo(Writer device, int padding, boolean newLine)
261        throws IOException
262      {
263        writeHtmlTo(device, padding, 1, newLine);
264      }
265      
266      /**
267       * Non-public method that handles the writing.  This method also
268       * keeps track of the nesting of elements so that each nested
269       * element is padded by {@code padding}, relative to its container
270       * element.
271       */
272      void writeHtmlTo(Writer device, int padding, int depth, boolean newLine)
273        throws IOException
274      {
275        //Padding
276        device.write(getPadding(padding, depth));
277        
278        //Opening tag
279        device.write('<');
280        device.write(tag.toString());
281    
282        //Attributes
283        for (HtmlAttribute attr : attributes.values())
284        {
285          device.write(' ');
286          device.write(attr.toString());
287        }
288        
289        //Contents and/or closing tag
290        if (isSimple())
291        {
292          device.write("/>");
293        }
294        else
295        {
296          device.write('>');
297          writeContentsAsHtml(device, padding, depth);
298          device.write("</");
299          device.write(tag.toString());
300          device.write('>');
301        }
302        
303        //New line
304        if (newLine)
305          device.write(System.getProperty("line.separator"));
306      }
307      
308      /**
309       * Writes the contents of this element to the device.
310       * The contents of an element is the information between the close of the
311       * start tag and the open of end tag.
312       */
313      abstract void writeContentsAsHtml(Writer device, int padding, int depth)
314        throws IOException;
315      
316      /** Creates string of space characters.  Length = depth * spacesPerLevel. */
317      String getPadding(int spacesPerLevel, int depth)
318      {
319        StringBuilder buff = new StringBuilder();
320        
321        int spaceCount = spacesPerLevel * depth;
322        
323        for (int s=1; s <= spaceCount; s++)
324          buff.append(' ');
325        
326        return buff.toString();
327      }
328      
329      /** Replaces \u00A0 with replacement. */
330      void replaceUnicodeNbspWith(StringBuilder buff, String replacement)
331      {
332        int nbspPos = buff.indexOf(NBSP_UNICODE);
333    
334        while (nbspPos >= 0)
335        {
336          buff.deleteCharAt(nbspPos);
337          buff.insert(nbspPos, replacement);
338          nbspPos = buff.indexOf(NBSP_UNICODE);
339        }
340      }
341    }