1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: History.java 2090 2011-03-07 04:13:05Z dmsmith $
21   */
22  package org.crosswire.common.history;
23  
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  
30  import org.crosswire.common.util.EventListenerList;
31  
32  /**
33   * Maintains a navigable history of objects. This maintains a dated list of
34   * objects and a current navigation list.
35   * 
36   * @see gnu.lgpl.License for license details.<br>
37   *      The copyright to this program is held by it's authors.
38   * @author DM Smith [dmsmith555 at yahoo dot com]
39   */
40  
41  public class History {
42      /**
43       * Create an empty navigation and history list.
44       * 
45       */
46      public History() {
47          nav = new ArrayList<Object>();
48          history = new HashMap<Object, Long>();
49          listeners = new EventListenerList();
50      }
51  
52      /**
53       * Make a particular element in the navigation list the current item in
54       * history.
55       * 
56       * @param index
57       *            the index of item to make the last one in the back list, -1
58       *            (or lower) will put everything in the forward list. Indexes
59       *            beyond the end of the list will put everything in the back
60       *            list.
61       */
62      public Object select(int index) {
63          int i = index;
64  
65          // Adjust to be 1 based
66          int size = nav.size();
67  
68          if (i > size) {
69              i = size;
70          } else if (i < 1) {
71              i = 1;
72          }
73  
74          // Only fire history changes when there is a change.
75          if (i != backCount) {
76              backCount = i;
77              fireHistoryChanged();
78          }
79  
80          return getCurrent();
81      }
82  
83      /**
84       * Add an element to history. If the element is in the forward list, then it
85       * replaces everything in the forward list upto it. Otherwise, it replaces
86       * the forward list.
87       * 
88       * @param obj
89       *            the object to add
90       */
91      public void add(Object obj) {
92          Object current = getCurrent();
93  
94          // Don't add null objects or the same object.
95          if (obj == null || obj.equals(current)) {
96              return;
97          }
98  
99          // If we are adding the next element, then just advance
100         // otherwise ...
101         // Object next = peek(1);
102         // if (!obj.equals(next))
103         // {
104         int size = nav.size();
105         if (size > backCount) {
106             int pos = backCount;
107             while (pos < size && !obj.equals(nav.get(pos))) {
108                 pos++;
109             }
110             // At this point pos either == size or the element at pos matches
111             // what we are navigating to.
112             nav.subList(backCount, Math.min(pos, size)).clear();
113         }
114 
115         // If it matches, then we don't have to do anything more
116         if (!obj.equals(peek(1))) {
117             // then we add it
118             nav.add(backCount, obj);
119         }
120         // }
121 
122         backCount++;
123 
124         // and remember when we saw it
125         visit(obj);
126 
127         fireHistoryChanged();
128     }
129 
130     /**
131      * Get all the elements in "back" list.
132      * 
133      * @return the elements in the back list.
134      */
135     public List<Object> getPreviousList() {
136         if (backCount > 0) {
137             return Collections.unmodifiableList(nav.subList(0, backCount));
138         }
139         return Collections.emptyList();
140     }
141 
142     /**
143      * Get all the elements in the "forward" list.
144      * 
145      * @return the elements in the forward list.
146      */
147     public List<Object> getNextList() {
148         if (backCount < nav.size()) {
149             return Collections.unmodifiableList(nav.subList(backCount, nav.size()));
150         }
151         return Collections.emptyList();
152     }
153 
154     /**
155      * Increments the current history item by the given amount. Positive numbers
156      * are forward. Negative numbers are back.
157      * 
158      * @param i
159      *            the distance to travel
160      * @return the item at the requested location, or at the end of the list if
161      *         i is too big, or at the beginning of the list if i is too small,
162      *         otherwise null.
163      */
164     public Object go(int i) {
165         return select(backCount + i);
166     }
167 
168     /**
169      * Get the current item in the "back" list
170      * 
171      * @return the current item in the back list.
172      */
173     public Object getCurrent() {
174         if (!nav.isEmpty() && backCount > 0) {
175             return nav.get(backCount - 1);
176         }
177         return null;
178     }
179 
180     /**
181      * Get the current item in the "back" list
182      * 
183      * @param i
184      *            the distance to travel
185      * @return the requested item in the navigation list.
186      */
187     private Object peek(int i) {
188         int size = nav.size();
189         if (size > 0 && backCount > 0 && backCount + i <= size) {
190             return nav.get(backCount + i - 1);
191         }
192         return null;
193     }
194 
195     /**
196      * Add a listener for history events.
197      * 
198      * @param li
199      *            the interested listener
200      */
201     public synchronized void addHistoryListener(HistoryListener li) {
202         listeners.add(HistoryListener.class, li);
203     }
204 
205     /**
206      * Remove a listener of history events.
207      * 
208      * @param li
209      *            the disinterested listener
210      */
211     public synchronized void removeHistoryListener(HistoryListener li) {
212         listeners.remove(HistoryListener.class, li);
213     }
214 
215     /**
216      * Note that this object has been seen at this time.
217      * 
218      * @param obj
219      */
220     private void visit(Object obj) {
221         history.put(obj, Long.valueOf(System.currentTimeMillis()));
222     }
223 
224     /**
225      * Kick of an event sequence
226      */
227     private synchronized void fireHistoryChanged() {
228         // Guaranteed to return a non-null array
229         Object[] contents = listeners.getListenerList();
230 
231         // Process the listeners last to first, notifying
232         // those that are interested in this event
233         HistoryEvent ev = null;
234         for (int i = contents.length - 2; i >= 0; i -= 2) {
235             if (contents[i] == HistoryListener.class) {
236                 if (ev == null) {
237                     ev = new HistoryEvent(this);
238                 }
239 
240                 ((HistoryListener) contents[i + 1]).historyChanged(ev);
241             }
242         }
243     }
244 
245     /**
246      * The elements that can be navigated.
247      */
248     private List<Object> nav;
249 
250     /**
251      * A map of elements that have been seen so far to when they have been seen.
252      */
253     private Map<Object, Long> history;
254 
255     /**
256      * The number of elements in the "back" list.
257      */
258     private int backCount;
259 
260     /**
261      * Listeners that are interested when history has changed.
262      */
263     private EventListenerList listeners;
264 }
265