001    package edu.nrao.sss.html;
002    
003    import java.io.IOException;
004    import java.io.Writer;
005    import java.util.HashMap;
006    import java.util.Map;
007    
008    import javax.swing.text.html.HTML;
009    
010    /**
011     * A cell in an HTML table.
012     * <p>
013     * <b>Version Info:</b>
014     * <table style="margin-left:2em">
015     *   <tr><td>$Revision: 545 $</td></tr>
016     *   <tr><td>$Date: 2007-04-19 10:38:45 -0600 (Thu, 19 Apr 2007) $</td></tr>
017     *   <tr><td>$Author: dharland $</td></tr>
018     * </table></p>
019     * 
020     * @author David M. Harland
021     * @since 2007-03-15
022     */
023    public class HtmlTableCell
024      extends HtmlElement
025      implements Cloneable
026    {
027      Type          type;
028      HtmlTableRow  parentRow;         //Not directly publicly mutable
029      StringBuilder unparsedContents;
030    
031      /** Creates a new data cell. */
032      public HtmlTableCell()
033      {
034        this(Type.DATA);
035      }
036      
037      /**
038       * Creates a new data cell with the given contents.
039       * @param contents the contents of this cell.
040       */
041      public HtmlTableCell(String contents)
042      {
043        this(Type.DATA);
044        
045        unparsedContents.append(contents);
046      }
047      
048      /**
049       * Creates a new cell of the given type.
050       * 
051       * @param cellType the type of this cell, either data or header.
052       *                 If this value is <i>null</i>, this cell will
053       *                 be a data cell.
054       */
055      public HtmlTableCell(Type cellType)
056      {
057        super(HTML.Tag.TD);
058        
059        setType(cellType);  //tag c/b reset here
060        
061        parentRow        = null;
062        unparsedContents = new StringBuilder();
063      }
064      
065      /**
066       * Creates a new {@code cellType} cell with the given contents.
067       * 
068       * @param cellType the type of this cell, either data or header.
069       *                 If this value is <i>null</i>, this cell will
070       *                 be a data cell.
071       *                 
072       * @param contents the contents of this cell.
073       */
074      public HtmlTableCell(Type cellType, String contents)
075      {
076        this(cellType);
077        
078        unparsedContents.append(contents);
079      }
080    
081      //============================================================================
082      // 
083      //============================================================================
084    
085      /** Returns <i>false</i>. */
086      @Override
087      public boolean isSimple()  { return false; }
088    
089      /**
090       * Returns the row to which this cell belongs.
091       * 
092       * @return the row to which this cell belongs.
093       *         If this cell does not belong to a row, the returned
094       *         value will be <i>null</i>.
095       */
096      public HtmlTableRow getParentRow()
097      {
098        return parentRow;
099      }
100      
101      /**
102       * Returns the position of this cell in its row.
103       * The first position is zero.
104       * 
105       * @return the position of this cell in its row.
106       *         If this cell is not part of a row, the
107       *         value returned is -1.
108       */
109      public int getPositionInRow()
110      {
111        return (parentRow == null) ? -1 : parentRow.cells.indexOf(this);
112      }
113      
114      /**
115       * Sets the type of this cell.
116       * 
117       * @param newType the type of this cell.  If this value is <i>null</i>,
118       *                this cell will be set to a data cell.
119       */
120      public final void setType(Type newType)
121      {
122        type = (newType == null) ? Type.DATA : newType;
123        
124        if      (type.equals(Type.DATA  ))  tag = HTML.Tag.TD;
125        else if (type.equals(Type.HEADER))  tag = HTML.Tag.TH;
126        else
127          throw new RuntimeException("PROGRAMMER ERROR: " +
128            "No HTML.Tag mapping for unknown HtmlTableCell.Type = " + type);
129      }
130      
131      /**
132       * Returns the type of this cell.
133       * @return the type of this cell.
134       */
135      public Type getType()
136      {
137        return type;
138      }
139      
140      /**
141       * Returns the contents between the beginning and ending cell tags.
142       * 
143       * @return the contents between the beginning and ending cell tags.
144       *         The value returned is guaranteed to be non-null, but could
145       *         be empty.  It is also the object held internally by this
146       *         cell, so any alterations to it will be reflected herein.
147       */
148      public StringBuilder getUnparsedContents()
149      {
150        return unparsedContents;
151      }
152      
153      /**
154       * Returns the number of columns spanned by this cell.
155       * @return the number of columns spanned by this cell.
156       */
157      public int getColumnSpan()
158      {
159        return getSpan(getAttributeValue(HTML.Attribute.COLSPAN));
160      }
161      
162      /**
163       * Returns the number of rows spanned by this cell.
164       * @return the number of rows spanned by this cell.
165       */
166      public int getRowSpan()
167      {
168        return getSpan(getAttributeValue(HTML.Attribute.ROWSPAN));
169      }
170      
171      /** Does the work for getColumnSpan & getRowSpan. */
172      private int getSpan(String text)
173      {
174        int span;
175        
176        if (text.length() > 0)
177        {
178          try {
179            span = Integer.parseInt(text);
180          }
181          catch (NumberFormatException ex) {
182            span = 1;
183          }
184        }
185        else
186        {
187          span = 1;
188        }
189        
190        return span;
191      }
192      
193      /**
194       * Returns the numeric value of the width attribute of this cell.
195       * If this cell has no width attribute, the value returned is -1.
196       * 
197       * @return the numeric value of the width attribute of this cell, or -1 if
198       *         this cell has no width magnitude.
199       */
200      public int getWidthMagnitude()
201      {
202        String widthText = getAttributeValue(HTML.Attribute.WIDTH);
203    
204        int value;
205        
206        try
207        {                                              // \D means "non-digit"
208          value = Integer.parseInt(widthText.replaceAll("\\D", "").trim());
209        }
210        catch (NumberFormatException ex)
211        {
212          value = -1;
213        }
214        
215        return value;
216      }
217      
218      /**
219       * Returns the portion of this cell's width attribute that follows
220       * the numeric portion.  For example, if this cell has
221       * <tt>width="25%"</tt>, the value returned is "%".  If this cell has no
222       * width attribute, the value returned is the empty string (<tt>""</tt>).
223       * (Note that the empty string would also be returned if this cell
224       * has <tt>width="3"</tt>.)
225       * <p>
226       * <b>Note:</b> the algorithm used by this method assumes that the value
227       * of the width attribute, if present, is properly formed.  If it is not,
228       * the results of this method are unpredictable.</p>
229       *  
230       * @return the portion of this cell's width attribute that follows the numeric
231       *         portion, or the empty string if this cell has no width attribute.
232       */
233      public String getWidthSuffix()
234      {
235        String widthText = getAttributeValue(HTML.Attribute.WIDTH);
236        
237        return widthText.replaceAll("\\d", "").trim(); // \d means "digit"
238      }
239      
240      //============================================================================
241      // WRITING
242      //============================================================================
243      
244      @Override
245      void writeContentsAsHtml(Writer device, int padding, int depth) throws IOException
246      {
247        //java's HTML parser converts &nbsp; to character 0xA0; reverse this
248        StringBuilder buff = new StringBuilder(unparsedContents);
249        
250        replaceUnicodeNbspWith(buff, NBSP_HTML);
251        
252        device.write(buff.toString());
253      }
254      
255      //============================================================================
256      // 
257      //============================================================================
258      
259      /** Returns <i>true</i> if {@code o} is equal to this cell. */
260      @Override
261      public boolean equals(Object o)
262      {
263        //Quick exit if o is this
264        if (o == this)
265          return true;
266        
267        //Quick exit if o is null
268        if (o == null)
269          return false;
270        
271        //Quick exit if classes are different
272        if (!o.getClass().equals(this.getClass()))
273          return false;
274       
275        HtmlTableCell other = (HtmlTableCell)o;
276        
277        //Intential absence of: parentRow
278        
279        return this.type.equals(other.type) &&
280               this.attributes.equals(other.attributes) &&
281               this.unparsedContents.toString()
282                   .equals(other.unparsedContents.toString());
283      }
284      
285      /**
286       * Compares {@code other} to this cell for equality, ignoring the
287       * <tt>parentRow</tt> and the <tt>colspan</tt> and <tt>rowspan</tt>
288       * attributes.
289       * 
290       * @param other the cell to which the comparison is made.
291       * 
292       * @return <i>true</i> if {@code other} is equal to this one, according
293       *         to the definition above.
294       */
295      public boolean equalsIgnoreSpans(HtmlTableCell other)
296      {
297        //Compare everything but the attributes
298        if (!this.type.equals(other.type) ||
299            !this.unparsedContents.toString().
300                  equals(other.unparsedContents.toString()))
301          return false;
302        
303        //Remove COLSPAN & ROWSPAN, then compare remaining attributes
304        Map<HTML.Attribute, HtmlAttribute> thisAttr =
305          new HashMap<HTML.Attribute, HtmlAttribute>(this.attributes);
306        
307        Map<HTML.Attribute, HtmlAttribute> otherAttr =
308          new HashMap<HTML.Attribute, HtmlAttribute>(other.attributes);
309        
310        thisAttr.remove(HTML.Attribute.COLSPAN);
311        otherAttr.remove(HTML.Attribute.COLSPAN);
312        
313        thisAttr.remove(HTML.Attribute.ROWSPAN);
314        otherAttr.remove(HTML.Attribute.ROWSPAN);
315        
316        return thisAttr.equals(otherAttr);
317      }
318      
319      /**
320       * Compares {@code other} to this cell for equality, ignoring the
321       * <tt>parentRow</tt> and {@code attribute}.
322       * 
323       * @param other the cell to which the comparison is made.
324       * @param attribute the attribute to ignore in the comparison.
325       * @return <i>true</i> if {@code other} is equal to this one, according
326       *         to the definition above.
327       */
328      public boolean equalsIgnoreAttribute(HtmlTableCell  other,
329                                           HTML.Attribute attribute)
330      {
331        //Compare everything but the attributes
332        if (!this.type.equals(other.type) ||
333            !this.unparsedContents.toString()
334                 .equals(other.unparsedContents.toString()))
335          return false;
336        
337        //Remove attribute, then compare remaining attributes
338        Map<HTML.Attribute, HtmlAttribute> thisAttr =
339          new HashMap<HTML.Attribute, HtmlAttribute>(this.attributes);
340        
341        Map<HTML.Attribute, HtmlAttribute> otherAttr =
342          new HashMap<HTML.Attribute, HtmlAttribute>(other.attributes);
343        
344        thisAttr.remove(attribute);
345        otherAttr.remove(attribute);
346        
347        return thisAttr.equals(otherAttr);
348      }
349      
350      /** Returns a hash code value for this cell. */
351      @Override
352      public int hashCode()
353      {
354        //Taken from the Effective Java book by Joshua Bloch.
355        //The constants 17 & 37 are arbitrary & carry no meaning.
356        int result = 17;
357        
358        //You MUST keep this method in synch w/ equals
359        
360        result = 37 * result + type.hashCode();
361        result = 37 * result + attributes.hashCode();
362        result = 37 * result + unparsedContents.hashCode();
363        
364        return result;
365      }
366    
367      /**
368       * Returns a new copy of this cell.
369       * The copy is almost a deep copy.  The one exception is that
370       * the <tt>parentRow</tt> of the returned clone is <i>null</i>.
371       * <p>
372       * If anything goes wrong during the cloning procedure,
373       * a {@code RuntimeException} will be thrown.</p>
374       */
375      @Override
376      public HtmlTableCell clone()
377      {
378        HtmlTableCell clone = null;
379        
380        try
381        {
382          clone = (HtmlTableCell)super.clone();
383          
384          clone.parentRow       = null;
385          clone.unparsedContents = new StringBuilder(this.unparsedContents);
386          clone.attributes =
387            new HashMap<HTML.Attribute, HtmlAttribute>(this.attributes);
388        }
389        catch (Exception ex)
390        {
391          throw new RuntimeException(ex);
392        }
393        
394        return clone;
395      }
396      
397      //============================================================================
398      // 
399      //============================================================================
400    
401      /** Types of table cells. */
402      public enum Type
403      {
404        /**
405         * A header cell.  Corresponds to {@link javax.swing.text.html.HTML.Tag#TH}.
406         */
407        HEADER,
408    
409        /**
410         * A data cell.  Corresponds to {@link javax.swing.text.html.HTML.Tag#TD}.
411         */
412        DATA;
413      }
414    }