001/*
002 * Copyright (c) 2005 Jens Schou, Staffan Gustafsson, Bjorn Lanneskog, 
003 * Einar Pehrson and Sebastian Kekkonen
004 *
005 * This file is part of
006 * CleanSheets Extension for Test Cases
007 *
008 * CleanSheets Extension for Test Cases is free software; you can
009 * redistribute it and/or modify it under the terms of the GNU General Public
010 * License as published by the Free Software Foundation; either version 2 of
011 * the License, or (at your option) any later version.
012 *
013 * CleanSheets Extension for Test Cases is distributed in the hope that
014 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied
015 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016 * See the GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with CleanSheets Extension for Test Cases; if not, write to the
020 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
021 * Boston, MA  02111-1307  USA
022 */
023package csheets.ext.test;
024
025import java.io.IOException;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.SortedSet;
034
035import csheets.core.Cell;
036import csheets.core.Value;
037import csheets.ext.CellExtension;
038
039/**
040 * An extension of a cell in a spreadsheet, with support for test cases.
041 * @author Staffan Gustafsson
042 * @author Jens Schou
043 * @author Einar Pehrson
044 */
045public class TestableCell extends CellExtension {
046
047        /** The unique version identifier used for serialization */
048        private static final long serialVersionUID = -2626239432851585308L;
049
050        /** The cell's test case parameters */
051        private Set<TestCaseParam> tcParams = new HashSet<TestCaseParam>();
052
053        /** The cell's test cases */
054        private Set<TestCase> testCases = new HashSet<TestCase>();
055
056        /** The listeners registered to receive events from the testable cell */
057        private transient List<TestableCellListener> listeners
058                = new ArrayList<TestableCellListener>();
059        
060        /**
061         * Creates a testable cell extension for the given cell.
062         * @param cell the cell to extend
063         */
064        TestableCell(Cell cell) {
065                super(cell, TestExtension.NAME);
066        }
067
068
069/*
070 * DATA UPDATES
071 */
072
073
074        /**
075         * Invoked to indicate that the content of the cell in the spreadsheet was
076         * modified and that test cases and test case paremeters that depend on that
077         * data must be updated, and new ones generated.
078         */
079        public void contentChanged(Cell cell) {
080                if (getFormula() != null) {
081                        resetTestCases();
082                } else {
083                        removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
084                        testCases.clear();
085                }
086        }
087        
088        
089/*
090 * TEST CASE ACCESSORS
091 */
092        
093        
094        /**
095         * Returns the test cases for the cell, which consist of a predetermined
096         * value for each of the cell's precedents.
097         * @return the cell's test cases
098         */
099        public Set<TestCase> getTestCases(){
100                return testCases;
101        }
102        
103        /**
104         * Returns whether the cell has any test cases.
105         * @return true if the cell has any test cases
106         */
107        public boolean hasTestCases(){
108                return !testCases.isEmpty();
109        }
110        
111        /**
112         * Returns whether any of the cell's test cases have been rejected.
113         * @return true if any of the cell's test cases have been rejected
114         */
115        public boolean hasTestError() {
116                for (TestCase testCase : testCases)
117                        if (testCase.getValidationState() == TestCase.ValidationState.REJECTED)
118                                return true;
119                return false;
120        }
121        
122        /**
123         * Returns the testedness of the cell, i.e. the ratio of valid
124         * test cases to available test cases in the cell.
125         * @return a number between 0.0 and 1.0 denoting the level of testedness
126         */
127        public double getTestedness() {
128                if (hasTestCases()) {
129                        // Calculates and returns the testedness
130                        double nValid = 0;
131                        for (TestCase testCase : testCases)
132                                if (testCase.getValidationState() == TestCase.ValidationState.VALID)
133                                        nValid++;
134                        return nValid / testCases.size();
135                } else
136                        return 0d;
137        }
138        
139        
140/*
141 * TEST CASE MODIFIERS
142 */
143
144
145        /**
146         * Generates new test cases for the cell, provided that all its precedents
147         * have test case parameters.            
148         */
149        public void resetTestCases(){
150                boolean changed = false;
151
152                if(!testCases.isEmpty()) {
153                        testCases.clear();
154                        removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
155                        changed = true;
156                }
157
158                if(allPrecedentsHaveParams() && getPrecedents().size() > 0) {
159                        // We pick one precedent at random to initiate the set
160                        TestableCell firstPrec = (TestableCell)getPrecedents().first();
161                        Iterator<TestCaseParam> paramIt = firstPrec.getTestCaseParams().iterator();
162                        // make one extention in the set per parameter
163                        while(paramIt.hasNext()) {
164                                //extendTestCases takes care of the rest of the precedents
165                                extendTestCases(firstPrec, paramIt.next());
166                        }
167                        changed = true;
168                }
169
170                if(changed) {
171                        fireTestCasesChanged();
172                }
173        }
174        
175        protected void extendTestCases(TestableCell firstPrec, TestCaseParam param) {
176                SortedSet<Cell> precedents = getPrecedents();
177                precedents.remove(firstPrec);
178
179                // The first precedent initiates the set
180                // make one entry in the set for the parameter
181                Map<Cell, Value> caseMap = new HashMap<Cell, Value>();
182                caseMap.put(firstPrec, param.getValue());
183                
184                Set<Map<Cell, Value>> casesSet = createCasesSet(precedents, caseMap);
185
186                if(toTestCases(casesSet))
187                        fireTestCasesChanged();
188        }
189        
190        private Set<Map<Cell, Value>> createCasesSet(Set<Cell> precedents,
191                                                                                                         Map<Cell, Value> caseMap){
192                // Set to store all maps used to make test cases
193                Set<Map<Cell, Value>> casesSet = new HashSet<Map<Cell, Value>>();
194                casesSet.add(caseMap);
195
196                // Now, update casesSet for each precedent
197                for(Cell prec : precedents){
198                        // a temporary set to store new caseMaps during the iteration
199                        Set<Map<Cell, Value>> tempCasesSet
200                                = new HashSet<Map<Cell, Value>>();
201
202                        for(TestCaseParam precParam : ((TestableCell)prec).getTestCaseParams()){
203                                // for each test case param in the precedent
204                                for(Map<Cell, Value> item : casesSet){
205
206                                        // for every caseMap
207                                        // make a copy, add current precedent address and param
208                                        Map<Cell, Value> itemCopy = new HashMap<Cell, Value>();
209                                        itemCopy.putAll(item);
210                                        itemCopy.put(precParam.getCell(), precParam.getValue());
211                                        // add the copy to tempCasesSet
212                                        tempCasesSet.add(itemCopy);
213                                }
214                        }
215                        casesSet = tempCasesSet;
216                }
217                return casesSet;
218        }
219
220        private boolean toTestCases(Set<Map<Cell, Value>> casesSet){
221                boolean tcChanged = false;
222                // for every item in casesSet, make TestCase and add to testCases
223
224                for(Map<Cell, Value> aoMap : casesSet){
225                        Set<TestCaseParam> tcParams = new HashSet<TestCaseParam>();
226                        Set<Map.Entry<Cell, Value>> aoSet = aoMap.entrySet();
227
228                        for(Map.Entry<Cell, Value> entry : aoSet){
229                                tcParams.add(new TestCaseParam(
230                                        (TestableCell)entry.getKey().getExtension(TestExtension.NAME),
231                                        entry.getValue(), TestCaseParam.Type.DERIVED));
232                        }
233                        
234                        // Creates the test case
235                        TestCase testCase = new TestCase(this, tcParams);
236                        testCases.add(testCase);
237                        tcChanged = true;
238                }
239                return tcChanged;
240        }
241
242        
243        /*
244         * TEST CASE PARAMETER ACCESSORS
245         */
246        
247
248        /**
249         * Returns the cell's test case parameters.
250         * @return the cell's the test case parameters.
251         */
252        public Set<TestCaseParam> getTestCaseParams(){
253                return tcParams;
254        }
255        
256        /**
257         * Returns whether the cell has any test case parameters.
258         * @return true if the cell has any test case parameters
259         */
260        public boolean hasTestCaseParams(){
261                return !tcParams.isEmpty();
262        }
263        
264        /**
265         * Tests if all of the cells precedents have test case parameters.
266         * @return true if all of the cells precedents have test case parameters
267         */
268        protected boolean allPrecedentsHaveParams(){
269                for (Cell precedent : getPrecedents())
270                        if (!((TestableCell)precedent).hasTestCaseParams())
271                                return false;
272                return true;
273        }
274        
275        /*
276         * TEST CASE PARAMETER MODIFIERS
277         */
278        
279        
280        /**
281         * Add a test case parameter to the cell's set of test case parameters.
282         * On addition, the cell's dependents are notified.
283         * @param value the value of the test case parameter to be added
284         * @return the parameter that was added, or null if the cell already had an identical parameter
285         */
286        public TestCaseParam addTestCaseParam(Value value) throws DuplicateUserTCPException {
287
288                TestCaseParam param = null;
289                Iterator<TestCaseParam> it = tcParams.iterator();
290                while(it.hasNext()) {
291                        param = it.next();
292                        if(value.equals(param.getValue())) {
293                                if(param.isUserEntered()) {
294                                        throw new DuplicateUserTCPException(value,
295                                   "Cells cannot have duplicate user-entered test case parameters");
296                                }
297                                else {
298                                        param.setType(TestCaseParam.Type.USER_ENTERED, true);
299                                        return param;
300                                }
301                        }
302                }
303                return addTestCaseParam(value, TestCaseParam.Type.USER_ENTERED);
304        }
305        
306        /**
307         * Add a test case parameter to the cell's set of test case parameters.
308         * On addition, the cell's dependents are notified.
309         * @param value the value of the test case parameter to be added
310         * @param type the type of test case parameter
311         */
312        public TestCaseParam addTestCaseParam(Value value, TestCaseParam.Type type) {
313
314                TestCaseParam param = null;
315                Iterator<TestCaseParam> it = tcParams.iterator();
316                while(it.hasNext()) {
317                        param = it.next();
318                        if(value.equals(param.getValue())) {
319                                param.setType(type, true);
320                                return param;
321                        }
322                }
323                param = new TestCaseParam(this, value, type);
324                tcParams.add(param);
325                for (Cell dependent : getDependents()) {
326                        ((TestableCell)dependent).precedentAddedParam(this, param);
327                }
328                // Notifies listeners
329                fireTestCaseParametersChanged();
330
331                        return param;
332        }
333        
334        /**
335         * Removes a test case parameter from the cell's set of test case parameters.
336         * On removal, the cell's dependents are notified.
337         * @param param the test case parameter to be removed
338         */
339        public void removeTestCaseParam(TestCaseParam param) {
340                removeTestCaseParam(param, TestCaseParam.Type.USER_ENTERED);
341        }
342
343        /**
344         * Removes a test case parameter from the cell's set of test case parameters.
345         * On removal, the cell's dependents are notified.
346         * @param param the test case parameter to be removed
347         * @param type the type of the parameter to remove
348         */
349        public void removeTestCaseParam(TestCaseParam param, TestCaseParam.Type type) {
350
351                // hitta param, toggla av type          
352                param.setType(type, false);
353                // om param har inga type -> ta bort och meddela dependents
354                if(param.hasNoType()) {
355                        tcParams.remove(param);
356
357                        // Notifies the cell's dependents
358                        for (Cell dependent : getDependents()){
359                                ((TestableCell)dependent).precedentRemovedParam(this, param);
360                        }  
361                        // Notifies listeners           
362                        fireTestCaseParametersChanged();
363                }
364        }
365        
366        protected void removeAllTcpsOfType(TestCaseParam.Type type) {
367                Iterator<TestCaseParam> tcpIt = tcParams.iterator();
368                // for all params...
369                while(tcpIt.hasNext()){
370                        TestCaseParam tcp = tcpIt.next();
371                        // ...check those of the specified type
372                        tcp.setType(type, false);
373                        if(tcp.hasNoType()){
374                                tcpIt.remove();
375
376                                // ...for all dependents...
377                                for (Cell dependent : getDependents())
378                                        // ...I no longer have this param
379                                        ((TestableCell)dependent).precedentRemovedParam(this, tcp);
380
381                                // Notifies listeners           
382                                fireTestCaseParametersChanged();
383                        }
384                }
385        }
386        
387        
388        /*
389         * TEST CASE PARAMETER UPDATES
390         */
391
392        
393        /**
394         * Invoked when a test case parameter is added to one of the cell's
395         * precedents. This causes the cell's test cases to be updated.
396         * @param cell the precedent to which the test case parameter was added
397         * @param param the test case parameter that was added
398         */
399        public void precedentAddedParam(TestableCell cell, TestCaseParam param) {
400                /*
401         * We only need to do anything if all our precedents have params
402         */
403                if (allPrecedentsHaveParams()) {
404                        /*
405         * if we don't have any test cases, we just make a whole new set
406         */
407                        if(testCases.isEmpty())
408                                resetTestCases();
409                        /*
410         * if test cases exist, we want to keep the old, and just update
411         * with the new test cases generated by the new param
412         */
413                        else {
414                                extendTestCases(cell, param);
415                        }
416                }
417        }
418        
419        /**
420         * Invoked when a test case parameter is removed from one of the cell's
421         * precedents. This causes the cell's test cases to be updated.
422         * @param cell the precedent from which the test case parameter was removed
423         * @param param the test case parameter that was removed
424         */
425        public void precedentRemovedParam(TestableCell cell, TestCaseParam param){
426                /*
427                 * if all precedents still have params, just remove the test cases
428                 * pertaining to the removed parameter.
429                 */
430                if(allPrecedentsHaveParams()){
431                        // iterate the test cases.
432                        Iterator<TestCase> tcIt =  testCases.iterator();
433                        
434                        // remove all test cases that used param as a parameter:
435                        while(tcIt.hasNext()){
436                                TestCase tCase = tcIt.next();
437                                Set<TestCaseParam> paramMap = tCase.getParams();
438                                if(paramMap.contains(param)){ // testcase uses removed param
439                                        tcIt.remove(); // remove the test case
440                                        TestCaseParam derivedParam = null;
441                                        Iterator<TestCaseParam> it = tcParams.iterator();
442                                        while(it.hasNext()) {
443                                                derivedParam = it.next();
444                                                if(tCase.evaluate().equals(derivedParam.getValue()))
445                                                        break;
446                                        }
447                                        if(derivedParam != null)
448                                                removeTestCaseParam(derivedParam, TestCaseParam.Type.DERIVED);
449                                }
450                        }
451                        fireTestCasesChanged();
452                }
453                        /*  If some precedents lack params, our dependents need to be notified
454                         * that all our derived params no longer apply (except for the derived
455                         * params that happen to be the same as a local param).
456                         * (no need to notify if we don't have any test cases, since then we
457                         * dn't have any derived params either) */
458                else if(!testCases.isEmpty()){
459                        testCases.clear();
460                        // inga test cases -> inga derived test case params
461                        removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
462                        fireTestCasesChanged();
463                }
464        }
465
466
467        /*
468         * CLIPBOARD
469         */
470        
471        
472        /**
473         * Removes the test case parameters from the cell.
474         * @param cell the cell that was modified
475         */
476        public void cellCleared(Cell cell) {
477                if (this.getDelegate().equals(cell)) {
478                        tcParams.clear();
479                }
480        }
481
482        /**
483         * Copies the user-specified test case parameters from the source cell to
484         * this one.
485         * @param cell the cell that was modified
486         * @param source the cell from which data was copied
487         */
488        public void cellCopied(Cell cell, Cell source) {
489                if (this.getDelegate().equals(cell)) {
490                        TestableCell testableSource = (TestableCell)source.getExtension(
491                                TestExtension.NAME);
492                        tcParams.clear();
493                        for (TestCaseParam param : testableSource.getTestCaseParams())
494                                if (param.hasType(TestCaseParam.Type.USER_ENTERED))
495                                        try {
496                                                addTestCaseParam(param.getValue());
497                                        } catch (DuplicateUserTCPException e) {}
498                }
499        }
500
501
502        /*
503         * EVENT LISTENING SUPPORT
504         */
505        
506        
507        /**
508         * Registers the given listener on the cell.
509         * @param listener the listener to be added
510         */
511        public void addTestableCellListener(TestableCellListener listener) {
512                listeners.add(listener);
513        }
514
515        /**
516         * Removes the given listener from the cell.
517         * @param listener the listener to be removed
518         */
519        public void removeTestableCellListener(TestableCellListener listener) {
520                listeners.remove(listener);
521        }
522
523        /**
524        * Notifies all registered listeners that the cell's test cases changed.
525        */
526        protected void fireTestCasesChanged() {
527                for (TestableCellListener listener : listeners)
528                        listener.testCasesChanged(this);
529        }
530
531        /**
532        * Notifies all registered listeners that the cell's test case parameters changed.
533        */
534        protected void fireTestCaseParametersChanged() {
535                for (TestableCellListener listener : listeners)
536                        listener.testCaseParametersChanged(this);
537        }
538
539        /**
540         * Customizes serialization, by recreating the listener list.
541         * @param stream the object input stream from which the object is to be read
542         * @throws IOException If any of the usual Input/Output related exceptions occur
543         * @throws ClassNotFoundException If the class of a serialized object cannot be found.
544         */
545        private void readObject(java.io.ObjectInputStream stream)
546                        throws java.io.IOException, ClassNotFoundException {
547            stream.defaultReadObject();
548                listeners = new ArrayList<TestableCellListener>();
549        }
550}