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    }