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 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 }