001/* 002 * Copyright (c) 2005 Einar Pehrson <einar@pehrson.nu>. 003 * 004 * This file is part of 005 * CleanSheets - a spreadsheet application for the Java platform. 006 * 007 * CleanSheets is free software; you can redistribute it and/or modify 008 * it under the terms of the GNU General Public License as published by 009 * the Free Software Foundation; either version 2 of the License, or 010 * (at your option) any later version. 011 * 012 * CleanSheets is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU General Public License 018 * along with CleanSheets; if not, write to the Free Software 019 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 020 */ 021package csheets.core; 022 023import java.io.IOException; 024import java.io.ObjectInputStream; 025import java.io.ObjectOutputStream; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.SortedSet; 031import java.util.TreeSet; 032 033import csheets.core.formula.Formula; 034import csheets.core.formula.Reference; 035import csheets.core.formula.compiler.FormulaCompilationException; 036import csheets.core.formula.compiler.FormulaCompiler; 037import csheets.core.formula.util.ReferenceTransposer; 038import csheets.ext.CellExtension; 039import csheets.ext.Extension; 040import csheets.ext.ExtensionManager; 041 042/** 043 * The implementation of the <code>Cell</code> interface. 044 * @author Einar Pehrson 045 */ 046public class CellImpl implements Cell { 047 048 /** The unique version identifier used for serialization */ 049 private static final long serialVersionUID = 926673794084390673L; 050 051 /** The spreadsheet to which the cell belongs */ 052 private Spreadsheet spreadsheet; 053 054 /** The address of the cell */ 055 private Address address; 056 057 /** The value of the cell */ 058 private Value value = new Value(); 059 060 /** The content of the cell */ 061 private String content = ""; 062 063 /** The cell's formula */ 064 private Formula formula; 065 066 /** The cell's precedents */ 067 private SortedSet<Cell> precedents = new TreeSet<Cell>(); 068 069 /** The cell's dependents */ 070 private SortedSet<Cell> dependents = new TreeSet<Cell>(); 071 072 /** The cell listeners that have been registered on the cell */ 073 private transient List<CellListener> listeners 074 = new ArrayList<CellListener>(); 075 076 /** The cell extensions that have been instantiated */ 077 private transient Map<String, CellExtension> extensions = 078 new HashMap<String, CellExtension>(); 079 080 /** 081 * Creates a new cell at the given address in the given spreadsheet. 082 * (not intended to be used directly). 083 * @see Spreadsheet#getCell(Address) 084 * @param spreadsheet the spreadsheet 085 * @param address the address of the cell 086 */ 087 CellImpl(Spreadsheet spreadsheet, Address address) { 088 this.spreadsheet = spreadsheet; 089 this.address = address; 090 } 091 092 /** 093 * Creates a new cell at the given address in the given spreadsheet, 094 * initialized with the given content (not intended to be used directly). 095 * @see Spreadsheet#getCell(Address) 096 * @param spreadsheet the spreadsheet 097 * @param address the address of the cell 098 * @param content the content of the cell 099 * @throws ExpressionSyntaxException if an incorrectly formatted formula was entered 100 */ 101 CellImpl(Spreadsheet spreadsheet, Address address, String content) throws FormulaCompilationException { 102 this(spreadsheet, address); 103 storeContent(content); 104 reevaluate(); 105 } 106 107/* 108 * LOCATION 109 */ 110 111 public Spreadsheet getSpreadsheet() { 112 return spreadsheet; 113 } 114 115 public Address getAddress() { 116 return address; 117 } 118 119/* 120 * VALUE 121 */ 122 123 public Value getValue() { 124 return value; 125 } 126 127 /** 128 * Updates the cell's value, and fires an event if it changed. 129 */ 130 private void reevaluate() { 131 Value oldValue = value; 132 133 // Fetches the new value 134 Value newValue; 135 if (formula != null) 136 try { 137 newValue = formula.evaluate(); 138 } catch (IllegalValueTypeException e) { 139 newValue = new Value(e); 140 } 141 else 142 newValue = Value.parseValue(content); 143 144 // Stores value 145 value = newValue; 146 147 // Checks for change 148 if (!newValue.equals(oldValue)) 149 fireValueChanged(); 150 } 151 152 /** 153 * Notifies all registered listeners that the value of the cell changed. 154 */ 155 private void fireValueChanged() { 156 for (CellListener listener : listeners) 157 listener.valueChanged(this); 158 for (CellExtension extension : extensions.values()) 159 extension.valueChanged(this); 160 161 // Notifies dependents of the changed value 162 for (Cell dependent : dependents) { 163 if (dependent instanceof CellImpl) 164 ((CellImpl)dependent).reevaluate(); 165 } 166 } 167 168/* 169 * CONTENT 170 */ 171 172 public String getContent() { 173 return content; 174 } 175 176 public Formula getFormula() { 177 return formula; 178 } 179 180 public void clear() { 181 try { 182 setContent(""); 183 } catch (FormulaCompilationException e) {} 184 fireCellCleared(); 185 } 186 187 public void setContent(String content) throws FormulaCompilationException { 188 if (!this.content.equals(content)) { 189 storeContent(content); 190 fireContentChanged(); 191 reevaluate(); 192 } 193 } 194 195 /** 196 * Updates the cell's content, and registers dependencies. 197 * @param content the content to store 198 * @throws FormulaCompilationException if an incorrectly formatted formula was entered 199 */ 200 private void storeContent(String content) throws FormulaCompilationException { 201 // Parses formula 202 Formula formula = null; 203 if (content.length() > 1) 204 formula = FormulaCompiler.getInstance().compile(this, content); 205 206 // Stores content and formula 207 this.content = content; 208 this.formula = formula; 209 updateDependencies(); 210 } 211 212 /** 213 * Updates the cell's dependencies. 214 */ 215 private void updateDependencies() { 216 // Deregisters as dependent with each old precedent 217 for (Cell precedent : precedents) 218 ((CellImpl)precedent).removeDependent(this); 219 precedents.clear(); 220 221 if (formula != null) 222 // Registers as dependent with each new precedent 223 for (Reference reference : formula.getReferences()) 224 for (Cell precedent : reference.getCells()) { 225 if (!this.equals(precedent)) { 226 precedents.add(precedent); 227 ((CellImpl)precedent).addDependent(this); 228 } 229 } 230 } 231 232 /** 233 * Notifies all registered listeners that the content of the cell changed. 234 */ 235 private void fireContentChanged() { 236 for (CellListener listener : listeners) 237 listener.contentChanged(this); 238 for (CellExtension extension : extensions.values()) 239 extension.contentChanged(this); 240 } 241 242 /** 243 * Notifies all registered listeners that the cell was cleared. 244 */ 245 private void fireCellCleared() { 246 for (CellListener listener : listeners) 247 listener.cellCleared(this); 248 for (CellExtension extension : extensions.values()) 249 extension.cellCleared(this); 250 } 251 252/* 253 * DEPENDENCIES 254 */ 255 256 public SortedSet<Cell> getPrecedents() { 257 return new TreeSet<Cell>(precedents); 258 } 259 260 public SortedSet<Cell> getDependents() { 261 return new TreeSet<Cell>(dependents); 262 } 263 264 /** 265 * Adds the given cell as a dependent of this cell, to be notified when its 266 * value changes. 267 * @param cell the dependent to add 268 */ 269 private void addDependent(Cell cell) { 270 dependents.add(cell); 271 fireDependentsChanged(); 272 } 273 274 /** 275 * Removes the given cell as a dependent of this cell. 276 * @param cell the dependent to remove 277 */ 278 private void removeDependent(Cell cell) { 279 dependents.remove(cell); 280 fireDependentsChanged(); 281 } 282 283 /** 284 * Notifies all registered listeners that the content of the cell changed. 285 */ 286 private void fireDependentsChanged() { 287 for (CellListener listener : listeners) 288 listener.dependentsChanged(this); 289 for (CellExtension extension : extensions.values()) 290 extension.dependentsChanged(this); 291 } 292 293/* 294 * CLIPBOARD 295 */ 296 297 public void copyFrom(Cell source) { 298 // Copies content 299 if (source.getFormula() == null) 300 try { 301 setContent(source.getContent()); 302 } catch (FormulaCompilationException e) {} 303 else { 304 // Copies and transposes formula 305 this.formula = new Formula(this, 306 new ReferenceTransposer( 307 getAddress().getColumn() - source.getAddress().getColumn(), 308 getAddress().getRow() - source.getAddress().getRow() 309 ).getExpression(source.getFormula().getExpression()) 310 ); 311 this.content = source.getContent().charAt(0) + formula.toString(); 312 updateDependencies(); 313 fireContentChanged(); 314 reevaluate(); 315 } 316 fireCellCopied(source); 317 } 318 319 public void moveFrom(Cell source) { 320 // Change the address of the source cell 321 // Remove the target cell from the spreadsheet 322 // Flag the target cell as overwritten! 323 324 // fireCellCopied(source); 325 } 326 327 /** 328 * Notifies all registered listeners that the cell was copied (or moved). 329 * @param source the cell from which data was copied 330 */ 331 private void fireCellCopied(Cell source) { 332 for (CellListener listener : listeners) 333 listener.cellCopied(this, source); 334 for (CellExtension extension : extensions.values()) 335 extension.cellCopied(this, source); 336 } 337 338/* 339 * EVENT HANDLING 340 */ 341 342 public void addCellListener(CellListener listener) { 343 listeners.add(listener); 344 } 345 346 public void removeCellListener(CellListener listener) { 347 listeners.remove(listener); 348 } 349 350 public CellListener[] getCellListeners() { 351 return listeners.toArray(new CellListener[listeners.size()]); 352 } 353 354/* 355 * EXTENSIONS 356 */ 357 358 public Cell getExtension(String name) { 359 // Looks for an existing cell extension 360 CellExtension extension = extensions.get(name); 361 if (extension == null) { 362 // Creates a new cell extension 363 Extension x = ExtensionManager.getInstance().getExtension(name); 364 if (x != null) { 365 extension = x.extend(this); 366 if (extension != null) 367 extensions.put(name, extension); 368 } 369 } 370 return extension; 371 } 372 373/* 374 * GENERAL 375 */ 376 377 /** 378 * Compares this cell with the specified cell for order, 379 * by comparing their addresses. 380 * @param cell the cell to be compared 381 * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. 382 */ 383 public int compareTo(Cell cell) { 384 if (spreadsheet != cell.getSpreadsheet()) 385 return -1; 386 else 387 return address.compareTo(cell.getAddress()); 388 } 389 390 /** 391 * Returns a string representation of the cell. 392 * @return the cell's content 393 */ 394 public String toString() { 395 return address.toString(); 396 } 397 398 /** 399 * Customizes deserialization by recreating the listener list and by catching 400 * exceptions when extensions are not found. 401 * @param stream the object input stream from which the object is to be read 402 * @throws IOException If any of the usual Input/Output related exceptions occur 403 * @throws ClassNotFoundException If the class of a serialized object cannot be found. 404 */ 405 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 406 stream.defaultReadObject(); 407 listeners = new ArrayList<CellListener>(); 408 409 // Reads extensions 410 extensions = new HashMap<String, CellExtension>(); 411 int extCount = stream.readInt(); 412 for (int i = 0; i < extCount; i++) { 413 try { 414 CellExtension extension = (CellExtension)stream.readObject(); 415 extensions.put(extension.getName(), extension); 416 } catch (ClassNotFoundException e) {} 417 } 418 } 419 420 /** 421 * Customizes serialization by writing extensions separately. 422 * @param stream the object output stream to which the object is to be written 423 * @throws IOException If any of the usual Input/Output related exceptions occur 424 */ 425 private void writeObject(ObjectOutputStream stream) throws IOException { 426 stream.defaultWriteObject(); 427 428 // Writes extensions 429 stream.writeInt(extensions.size()); 430 for (CellExtension extension : extensions.values()) 431 stream.writeObject(extension); 432 } 433}