001    package org.LiveGraph.dataCache;
002    
003    import java.io.PrintWriter;
004    import java.io.StringWriter;
005    import java.util.ArrayList;
006    import java.util.Arrays;
007    import java.util.Collections;
008    import java.util.EnumSet;
009    import java.util.Iterator;
010    import java.util.List;
011    import java.util.Set;
012    
013    import org.LiveGraph.LiveGraph;
014    import org.LiveGraph.events.Event;
015    import org.LiveGraph.events.EventProcessingException;
016    import org.LiveGraph.events.EventProducer;
017    import org.LiveGraph.events.EventType;
018    
019    
020    import com.softnetConsult.utils.collections.NullIterator;
021    import com.softnetConsult.utils.collections.ReadOnlyIterator;
022    import com.softnetConsult.utils.exceptions.UnexpectedSwitchCase;
023    
024    
025    /**
026     * An instance of this class caches datasets previously read from a data file in memory.
027     * The cache applies a smart procedure to cache just enough data in order to plot a graph
028     * on the screen. Two cache modes are currently possible: {@code CacheTailData} and
029     * {@code CacheAllData}. In the first case the data sets added most recently are
030     * cached (and ultimately displayed bythe plotter). In the latter case all datasets are
031     * cached. If the number of datasets grows too large, the datasets located at odd indices in
032     * the original data file will be deleted from the cache.
033     * After this only datasets located at even indices in the original file will be cached.
034     * If the cache grows too large again, this procedure is re-applied such that only datasets
035     * at indices divisible by 4 in the original file are cached. As more datasets are added to the
036     * cache, this procedure can be re-applied again making sure that at any time the original data
037     * file is sampled at equal intervals.<br />
038     * The maximum cache size is {@code CACHE_SIZE}.  
039     * 
040     * <p>
041     *   <strong>LiveGraph</strong>
042     *   (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>).
043     * </p> 
044     * <p>Copyright (c) 2007 by G. Paperin.</p>
045     * <p>File: DataCache.java</p>
046     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
047     *    without modification, are permitted provided that the following terms and conditions are met:
048     * </p>
049     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
050     *    acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
051     *    this list of conditions and the following disclaimer.<br />
052     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
053     *    LiveGraph project and its web-site, the above copyright notice, this list of conditions
054     *    and the following disclaimer in the documentation and/or other materials provided with
055     *    the distribution.<br />
056     *    3. All advertising materials mentioning features or use of this software or any derived
057     *    software must display the following acknowledgement:<br />
058     *    <em>This product includes software developed by the LiveGraph project and its
059     *    contributors.<br />(http://www.live-graph.org)</em><br />
060     *    4. All advertising materials distributed in form of HTML pages or any other technology
061     *    permitting active hyper-links that mention features or use of this software or any
062     *    derived software must display the acknowledgment specified in condition 3 of this
063     *    agreement, and in addition, include a visible and working hyper-link to the LiveGraph
064     *    homepage (http://www.live-graph.org).
065     * </p>
066     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
067     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
068     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
069     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
070     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
071     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
072     * </p>
073     * 
074     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
075     * @version {@value org.LiveGraph.LiveGraph#version}
076     */
077    public class DataCache implements EventProducer {
078    
079    /**
080     * Maximum number if datasets to be held in memory.
081     */
082    public static final int CACHE_SIZE = 500;
083    
084    
085    /**
086     * Maximum number of delayed events during a bulk operation before the listeners are brought up to date.
087     */
088    public static final int MAX_DELAYED_EVENTS = 1000;
089    
090    /**
091     * Number of datasets to always keep in memory when operating in {@code CacheTailData}-mode.
092     */
093    public static final int TAIL_BALANCE_SIZE = (int) (0.75 * (double) CACHE_SIZE);
094    
095    /**
096     * Defines possible cache modes.
097     * {@code CacheAllData} specifies that all datasets ever added to this cache must be sampled
098     * at equal intervals. {@code CacheTailData} specifies that only the most recently datasets
099     * are to be kept in memory.
100     */
101    public static enum CacheMode { CacheAllData, CacheTailData };
102    
103    
104    /**
105     * Stores the desctibtion of the data series in this cache. A data series corresponds to a column
106     * in a data file.
107     */
108    private List<DataSeries> dataSeries = null;
109    
110    /**
111     * Stores the data in this cache.
112     */
113    private List<DataSet> dataSets = null;
114    
115    /**
116     * Current operating mode.
117     */
118    private CacheMode currentMode = null;
119    
120    /**
121     * When working in {@code CacheAllData}-mode this value determines which datasets are kept in memory.
122     * At any time, exactly the datasets for which {@link DataSet#getDataFileIndex()} returns a value that
123     * can be divided by {@code dispersalFactor} without remainder will be kept in the cache. 
124     */
125    private int dispersalFactor = 1;
126    
127    /**
128     * Caches the data file info lines.
129     */
130    private List<String> dataFileInfo = null;
131    
132    /**
133     * Caches the smallest data value currently in the cache.
134     */
135    private double minValueCached = Double.NaN;
136    
137    /**
138     * Caches the largest data value currently in the cache.
139     */
140    private double maxValueCached = Double.NaN;
141    
142    /**
143     * Stores occured cache events when operating in the delayed mode.
144     */
145    private Set<CacheEvent> delayedEvents = null;
146    
147    /**
148     * Whether the cache events are being delayed. 
149     */
150    private boolean delayEvents = false;
151    
152    /**
153     * Number of events that has been delayed during the current bulk operation.
154     */
155    private int countDelayedEvents = 0;
156    
157    /**
158     * Creates a new data cache in the {@code CacheAllData}-mode.
159     *
160     */
161    public DataCache() {
162            this.delayedEvents = EnumSet.noneOf(CacheEvent.class);
163            this.delayEvents = false;
164            this.countDelayedEvents = 0;
165            this.resetCache(CacheMode.CacheAllData);
166    }
167    
168    /**
169     * Creates a new cache in the specified mode.
170     * 
171     * @param mode Mode of the new cache.
172     */
173    public DataCache(CacheMode mode) {
174            this(); 
175            this.resetCache(mode);
176    }
177    
178    /**
179     * Creates a new cache in a specified mode and initilises it for the specified data series.
180     * 
181     * @param mode Mode to use.
182     * @param seriesLabels Names of the data series.
183     */
184    public DataCache(CacheMode mode, List<String> seriesLabels) {
185            this(); 
186            resetCache(mode, seriesLabels);
187    }
188    
189    /**
190     * Creates a new cache in a specified mode and initilises it for the specified data series.
191     * 
192     * @param mode Mode to use.
193     * @param seriesLabels Names of the data series.
194     */
195    public DataCache(CacheMode mode, String [] seriesLabels) {
196            this(mode, Arrays.asList(seriesLabels));
197    }
198    
199    /**
200     * Removes all data from this cache and resets is to the empty state.
201     */
202    public void resetCache() {
203            resetLabels();
204            resetData();
205            resetDataFileInfo();
206    }
207    
208    /**
209     * Removes all data from this cache and resets is to the empty state.
210     * 
211     * @param mode The mode the cache will have after the reset.
212     */
213    public void resetCache(CacheMode mode) {
214            resetLabels();
215            resetData(mode);
216            resetDataFileInfo();
217    }
218    
219    /**
220     * Removes all data from this cache and resets is to the empty state.
221     * New data series are set up according to the specified labels.
222     * 
223     * @param mode The mode the cache will have after the reset.
224     * @param seriesLabels The data series labels for the reset cache
225     */
226    public void resetCache(CacheMode mode, List<String> seriesLabels) {
227            resetLabels(seriesLabels);
228            resetData(mode);
229            resetDataFileInfo();
230    }
231    
232    /**
233     * Removes all data series informatioon from the cache without deleting the actual data.
234     */
235    public void resetLabels() {     
236            List<String> l = Collections.emptyList();
237            resetLabels(l);
238    }
239    
240    /**
241     * Removes all data series informatioon from the cache and replaces is with new empty series.
242     * Actual data is not affected.
243     * 
244     * @param seriesLabels Labels for the new data series.
245     */
246    public void resetLabels(List<String> seriesLabels) {
247            
248            if (null == seriesLabels)
249                    throw new NullPointerException("Series labels array cannot be null"); 
250            
251            dataSeries = new ArrayList<DataSeries>(seriesLabels.size());
252            int index = 0;
253            for (String label : seriesLabels) {
254                    dataSeries.add(new DataSeries(label, this, index));
255                    index++;
256            }
257            
258            fireEvent(CacheEvent.CACHE_UpdatedLabels);
259    }
260    
261    /**
262     * Resets the cache while keeping the same operating mode.
263     * All data is deleted.
264     */
265    public void resetData() {
266            resetData(currentMode);
267    }
268    
269    /**
270     * Resets the cache to the specified mode. All data is deleted.
271     * 
272     * @param mode New operating mode.
273     */
274    public void resetData(CacheMode mode) {
275            
276            if (null == mode)
277                    throw new NullPointerException("Cache mode cannot be null");
278            
279            if (null != dataSets) {
280                    dataSets.clear();
281                    dataSets = null;
282            }
283            
284            currentMode = mode;
285            
286            resetExtremeValues();
287            
288            switch (currentMode) {
289                    case CacheAllData:      dataSets = new RemoveRangeArrayList<DataSet>(CACHE_SIZE);
290                                                            dispersalFactor = 1;
291                                                            break;
292                                                            
293                    case CacheTailData:     dataSets = new RemoveRangeArrayList<DataSet>(CACHE_SIZE);
294                                                            dispersalFactor = 1;
295                                                            break;
296                    
297                    default:
298                            throw new UnexpectedSwitchCase(currentMode);
299            }
300            
301            fireEvent(CacheEvent.CACHE_ChangedMode);
302            fireEvent(CacheEvent.CACHE_UpdatedData);
303    }
304    
305    /**
306     * Delets the information of min and max values held by this cache and any of its data series.
307     *
308     */
309    private void resetExtremeValues() {
310            minValueCached = Double.NaN;
311            maxValueCached = Double.NaN;
312            for (DataSeries s : dataSeries)
313                    s.resetExtremeValues();
314    }
315    
316    /**
317     * Delets all data file info strings held by this cache.
318     *
319     */
320    public void resetDataFileInfo() {
321            this.dataFileInfo = new ArrayList<String>();
322            fireEvent(CacheEvent.CACHE_UpdatedDataFileInfo);
323    }
324    
325    /**
326     * @return Current operating mode.
327     */
328    public CacheMode getCacheMode() {
329            return currentMode;
330    }
331    
332    /**
333     * @return Number of data series in the cache (i.e. data columns in the data file).
334     */
335    public int countDataSeries() {
336            return dataSeries.size();
337    }
338    
339    /**
340     * @return a Read-olny iterator over this cache's data series.
341     */
342    public ReadOnlyIterator<DataSeries> iterateDataSeries() {
343            return new ReadOnlyIterator<DataSeries>(dataSeries.iterator());
344    }
345    
346    /**
347     * @param index Data series number (0-based).
348     * @return The data series at the specified index.
349     */
350    public DataSeries getDataSeries(int index) {
351            return dataSeries.get(index);
352    }
353    
354    /**
355     * @return An read-only iterator over the labels of the data series in this cache.
356     */
357    public ReadOnlyIterator<String> iterateDataSeriesLabels() {
358            return new DataSeriesLabelIterator(dataSeries.iterator());
359    }
360    
361    /** 
362     * @param label A data series label.
363     * @return The index of the series with the specified label or -1 if not found.
364     */
365    public int findDataSeriesIndex(String label) {
366            int index = 0;
367            ReadOnlyIterator<String> it = iterateDataSeriesLabels();
368            while (it.hasNext()) {                          
369                    if (it.next().equals(label))
370                            return index;
371                    index++;
372            }
373            return -1;
374    }
375    
376    /** 
377     * @param label A data series label.
378     * @param ignoreCase Whether case shuld be ignore in string comparison. 
379     * @return The index of the series with the specified label or -1 if not found.
380     */
381    public int findDataSeriesIndex(String label, boolean ignoreCase) {
382            int index = 0;
383            ReadOnlyIterator<String> it = iterateDataSeriesLabels();
384            while (it.hasNext()) {          
385                    if (ignoreCase && it.next().equalsIgnoreCase(label))
386                            return index;
387                    if (!ignoreCase && it.next().equals(label))
388                            return index;
389                    index++;
390            }
391            return -1;
392    }
393    
394    /**
395     * @return Number of datasets currently in cache.
396     */
397    public int countDataSets() {
398            return dataSets.size();
399    }
400    
401    /**
402     * @return Read-only iterator over the datasets in this cache.
403     */
404    public ReadOnlyIterator<DataSet> iterateDataSets() {
405            return new ReadOnlyIterator<DataSet>(dataSets.iterator());
406    }
407    
408    /**
409     * @param cacheIndex Cache-index of a dataset.
410     * @return Dataset at the specified index.
411     */
412    public DataSet getDataSet(int cacheIndex) {
413            return dataSets.get(cacheIndex);
414    }
415    
416    /**
417     * @return Smallest value currently in the cache or {@code Double.NaN} if the cache is empty.
418     */
419    public double getMinValueCached() {
420            return minValueCached;
421    }
422    
423    /**
424     * @return Largest value currently in the cache or {@code Double.NaN} if the cache is empty.
425     */
426    public double getMaxValueCached() {
427            return maxValueCached;
428    }
429    
430    /**
431     * @return The index which the first dataset in this chache had in the original datafile.
432     */
433    public int getMinDataFileIndex() {
434            try {
435                    return dataSets.get(0).getDataFileIndex();
436            } catch (IndexOutOfBoundsException e) {
437                    return 0;
438            } catch (NullPointerException e) {
439                    return 0;
440            }
441    }
442    
443    /**
444     * @return The index that the last dataset in this chache had in the original datafile.
445     */
446    public int getMaxDataFileIndex() {
447            try {
448                    return dataSets.get(dataSets.size() - 1).getDataFileIndex();
449            } catch (IndexOutOfBoundsException e) {
450                    return 0;
451            } catch (NullPointerException e) {
452                    return 0;
453            }
454    }
455    
456    /**
457     * @param dataFileIndex An index in the original datafile.
458     * @return A dataset which was located at the specified index in the original data file, or {@code null}
459     * if there is no such dataset in the cache.
460     */
461    public DataSet findDataSet(int dataFileIndex) {
462            int cacheIndex = Collections.binarySearch(dataSets, dataFileIndex);
463            if (cacheIndex < 0)
464                    return null;
465            if (cacheIndex >= dataSets.size())
466                    return null;
467            DataSet ds = getDataSet(cacheIndex);
468            if (ds.getDataFileIndex() != dataFileIndex)
469                    return null;
470            return ds;
471    }
472    
473    /**
474     * Adds a specified dataset to this cache.
475     * @param ds A dataset.
476     */
477    public void addDataSet(DataSet ds) {
478            
479            // Ignore null datasets:
480            if (null == ds)
481                    return;
482            
483            // Add dataset according to the current cache mode:
484            boolean reallyAdded = false;
485            switch (currentMode) {
486                    case CacheAllData:      reallyAdded = addDataSet_AllDataMode(ds);
487                                                            break;
488                    case CacheTailData:     reallyAdded = addDataSet_TailDataMode(ds);
489                                                            break;
490            }
491            
492            // If the dataset has not actually been cached, there is nothing more to so:
493            if (!reallyAdded)
494                    return;
495            
496            // Update min- and max-caches:
497            includeExtremeValues(ds);
498            
499            // Notify listeners:
500            fireEvent(CacheEvent.CACHE_UpdatedData);
501    }
502    
503    /**
504     * Updates the internal state of this cache and its data series to include the min and max
505     * values of the specified dataset.
506     * @param ds A dataset.
507     */
508    private void includeExtremeValues(DataSet ds) {
509            
510            for (int s = 0; s < dataSeries.size(); s++) {
511                    
512                    double val = ds.getValue(s);
513                    
514                    if (Double.isNaN(val) || Double.isInfinite(val))
515                            continue;
516                    
517                    if (val < minValueCached || Double.isNaN(minValueCached))
518                            minValueCached = val;
519                    
520                    if (val > maxValueCached || Double.isNaN(maxValueCached))
521                            maxValueCached = val;
522                    
523                    dataSeries.get(s).includeExtremeValue(val);
524            }
525    }
526    
527    /**
528     * Adds a dataset when cache is in {@code AllDataMode}.
529     * @param ds A dataset.
530     * @return Whether the dataset was actually added.
531     */
532    private boolean addDataSet_AllDataMode(DataSet ds) {
533            
534            if (0 != (ds.getDataFileIndex() % dispersalFactor))
535                    return false;
536            
537            if (CACHE_SIZE > dataSets.size()) {
538                    dataSets.add(ds);
539                    return true;
540            }
541            
542            increaseDispersalFactor();
543            return addDataSet_AllDataMode(ds);
544    }
545    
546    /**
547     * Increases the value which must divide datafile indices of all cached datasets without remainder.
548     * Datasets with the wrong datafile indices are removed from the cache and the cache indices are updated.
549     * This method is used to compact the cache in {@code AllDataMode}-mode.
550     */
551    private void increaseDispersalFactor() {
552            
553            // Remove every second dataset:
554            int i = 0;
555            boolean remove = false;
556            while (i < dataSets.size()) {
557                    if (remove)
558                            dataSets.remove(i);
559                    else
560                            i++;
561                    remove = !remove;
562            }
563            
564            if (dataSets instanceof ArrayList)      
565                    ((ArrayList) dataSets).ensureCapacity(CACHE_SIZE);
566            
567            // Increase dispersal factor:
568            dispersalFactor *= 2;
569            
570            // Rebuild the extreme values cache:
571            resetExtremeValues();
572            for (DataSet ds : dataSets)
573                    includeExtremeValues(ds);
574    }
575    
576    /**
577     * Adds a dataset when cache is in {@code TailDataMode}.
578     * @param ds A dataset.
579     * @return {@code true}.
580     */
581    private boolean addDataSet_TailDataMode(DataSet ds) {
582            
583            if (CACHE_SIZE > dataSets.size()) {
584                    dataSets.add(ds);
585                    return true;
586            }
587            
588            removeDatalistHead();
589            return addDataSet_TailDataMode(ds);
590    }
591    
592    /**
593     * Removes the oldest datasets in this cache.
594     * This method is used to compact the cache in {@code AllDataMode}-mode.
595     */
596    private void removeDatalistHead() {
597            
598            // Remove datasets wich were cached the longes time ago:
599            
600            if (dataSets instanceof RemoveRangeArrayList) {
601                    ((RemoveRangeArrayList) dataSets).removeRangeint(0, CACHE_SIZE - TAIL_BALANCE_SIZE);
602                    
603            } else {
604                    while (TAIL_BALANCE_SIZE > dataSets.size())
605                            dataSets.remove(0);
606            }
607            
608            if (dataSets instanceof ArrayList)
609                    ((ArrayList) dataSets).ensureCapacity(CACHE_SIZE);
610            
611            // Rebuild the extreme values cache:
612            resetExtremeValues();
613            for (DataSet ds : dataSets)
614                    includeExtremeValues(ds);
615    }
616    
617    /**
618     * Caches info on the data file.
619     * @param info File info.
620     */
621    public void addDataFileInfo(String info) {
622            this.dataFileInfo.add(info);
623            fireEvent(CacheEvent.CACHE_UpdatedDataFileInfo);
624    }
625    
626    /**
627     * @return A list of all caches data file info strings.
628     */
629    public List<String> listDataFileInfo() {
630            return Collections.unmodifiableList(dataFileInfo);
631    }
632    
633    /**
634     * @return The data file info where all cached info strings are separated by new-lines and
635     * concatenated into a single string.
636     */
637    public String getDataFileInfo() {
638            
639            StringWriter s = new StringWriter();
640            PrintWriter w = new PrintWriter(s);
641            for (String infoLine : dataFileInfo)
642                    w.println(infoLine);
643            
644            w.close();
645            return s.toString();
646    }
647    
648    /**
649     * Objects of this class do not handle {@code eventProcessingFinished} notifications.
650     * 
651     * @param event Ignored.
652     */
653    public void eventProcessingFinished(Event<? extends EventType> event) { }
654    
655    /**
656     * Objects of this class do not handle {@code eventProcessingException} notofications.
657     * 
658     * @param event Ignored. 
659     * @param exception Never actually thrown.
660     * @return {@code false}.
661     */
662    public boolean eventProcessingException(Event<? extends EventType> event, EventProcessingException exception) {
663            return false;
664    }
665    
666    
667    /**
668     * Notifies the observers of a specified event. If this cache is currently in {@code delayEvents}
669     * mode, the observers are not notified and the event is cached. However, even in the
670     * {@code delayEvents}-mode, if the number of events delayed so far exceeds {@link #MAX_DELAYED_EVENTS},
671     * all events delayed so far <em>are</em> fired - this ensured that listeners are brought up to date
672     * from time to time during very long bulk operations.
673     * @param eventType An event type.
674     */
675    private void fireEvent(CacheEvent eventType) {
676            
677            if (null == eventType)
678                    return;
679            
680            if (!delayEvents) {
681                    raiseEvent(eventType);
682                    return;
683            }
684                            
685            delayedEvents.add(eventType);
686            countDelayedEvents++;
687            if (countDelayedEvents > MAX_DELAYED_EVENTS)
688                    raiseDelayedEvents();
689    }
690    
691    /**
692     * Notifies the observers of an event of a specified type.
693     * 
694     * @param eventType Type of event to raise;
695     */
696    private void raiseEvent(CacheEvent eventType) {
697            Event<CacheEvent> event = new Event<CacheEvent>(this, CacheEvent.class, eventType);
698            LiveGraph.application().eventManager().raiseEvent(event);
699    }
700    
701    /**
702     * When this method is invoked the cache enters the {@code delayEvents}-mode;
703     * while in this mode events are not supplied to observers, instead they are cached
704     * and fired only when {@code fireDelayedEvents} is invoked. This is can be useful
705     * when the cache is modified several times in one go. In such case the notification
706     * of observers can be consolidated which might save processing similar events many times.
707     * 
708     * @see #bulkOperationCompleted()
709     */
710    public void bulkOperationStart() {
711            if (!delayedEvents.isEmpty())
712                    raiseDelayedEvents();
713            delayEvents = true;
714    }
715    
716    /**
717     * Ends the {@code delayEvents}-mode and returns in the normal observable mode;
718     * all events cached while in that mode are fired. However, each type of event
719     * is fired at most once. The order in which the events are fires is unspecified
720     * and might not correspond to the order in which the events actually occured.  
721     */
722    public void bulkOperationCompleted() {
723            if (!delayEvents)
724                    return;
725            delayEvents = false;
726            raiseDelayedEvents();
727    }
728    
729    /**
730     * Raises all events delayed so far during a buld operation:
731     */
732    private void raiseDelayedEvents() {
733            
734            // We want to fire the events in this particular order:
735            
736            if (delayedEvents.contains(CacheEvent.CACHE_ChangedMode))
737                    raiseEvent(CacheEvent.CACHE_ChangedMode);
738                    
739            if (delayedEvents.contains(CacheEvent.CACHE_UpdatedDataFileInfo))
740                    raiseEvent(CacheEvent.CACHE_UpdatedDataFileInfo);
741            
742            if (delayedEvents.contains(CacheEvent.CACHE_UpdatedLabels))
743                    raiseEvent(CacheEvent.CACHE_UpdatedLabels);
744            
745            if (delayedEvents.contains(CacheEvent.CACHE_UpdatedData))
746                    raiseEvent(CacheEvent.CACHE_UpdatedData);
747            
748            // This is only required in case we forgot an event type:
749            for (CacheEvent eventType : delayedEvents) {
750                    if (CacheEvent.CACHE_ChangedMode != eventType
751                                    && CacheEvent.CACHE_UpdatedDataFileInfo != eventType 
752                                    && CacheEvent.CACHE_UpdatedLabels != eventType
753                                    && CacheEvent.CACHE_UpdatedData != eventType) {
754                            raiseEvent(eventType);
755                    }
756            }
757            
758            delayedEvents.clear();
759            countDelayedEvents = 0;
760    }
761    
762    /**
763     * A {@code ArrayList} which publicly publishes its {@code removeRangeint} method.
764     * @param <E> Any class.
765     */
766    private class RemoveRangeArrayList<E> extends ArrayList<E> {
767            public RemoveRangeArrayList() { super(); }
768            public RemoveRangeArrayList(int initialCapacity) { super(initialCapacity); }
769            public void removeRangeint(int fromIndex, int toIndex) { super.removeRange(fromIndex, toIndex); }
770    } // private class RemoveRangeArrayList<E>
771    
772    /**
773     * A read-only iterator for data series labels.
774     */
775    private class DataSeriesLabelIterator extends ReadOnlyIterator<String> {
776            private Iterator<DataSeries> iterator = null;
777            
778            public DataSeriesLabelIterator(Iterator<DataSeries> iter) {
779                    super(NullIterator.getSingeltonInstance(String.class));
780                    iterator = iter;
781            }
782            
783            @Override
784            public boolean hasNext() {
785                    return iterator.hasNext();
786            }
787            
788            @Override
789            public String next() {
790                    return iterator.next().getLabel();
791            }
792            
793            @Override
794            public void remove() {
795                    throw new UnsupportedOperationException("Cannot use this iterator to remove items.");       
796            }
797    } // class DataSeriesLabelIterator
798    
799    } // class DataCache