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}