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