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 */
021 package csheets.core;
022
023 import java.io.IOException;
024 import java.io.ObjectInputStream;
025 import java.io.ObjectOutputStream;
026 import java.util.ArrayList;
027 import java.util.HashMap;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.SortedSet;
031 import java.util.TreeSet;
032
033 import csheets.core.formula.Formula;
034 import csheets.core.formula.Reference;
035 import csheets.core.formula.compiler.FormulaCompilationException;
036 import csheets.core.formula.compiler.FormulaCompiler;
037 import csheets.core.formula.util.ReferenceTransposer;
038 import csheets.ext.CellExtension;
039 import csheets.ext.Extension;
040 import csheets.ext.ExtensionManager;
041
042 /**
043 * The implementation of the <code>Cell</code> interface.
044 * @author Einar Pehrson
045 */
046 public 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 }